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.

23094 lines
678 KiB

10 years ago
10 years ago
10 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.7.4
  8. * @date 2014-04-18
  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. if (Hex == "A")
  851. Value = 10;
  852. else if (Hex == "B")
  853. Value = 11;
  854. else if (Hex == "C")
  855. Value = 12;
  856. else if (Hex == "D")
  857. Value = 13;
  858. else if (Hex == "E")
  859. Value = 14;
  860. else if (Hex == "F")
  861. Value = 15;
  862. else
  863. Value = eval(Hex)
  864. return Value;
  865. };
  866. util.GiveHex = function GiveHex(Dec) {
  867. if (Dec == 10)
  868. Value = "A";
  869. else if (Dec == 11)
  870. Value = "B";
  871. else if (Dec == 12)
  872. Value = "C";
  873. else if (Dec == 13)
  874. Value = "D";
  875. else if (Dec == 14)
  876. Value = "E";
  877. else if (Dec == 15)
  878. Value = "F";
  879. else
  880. Value = "" + Dec;
  881. return Value;
  882. };
  883. /**
  884. * Parse a color property into an object with border, background, and
  885. * highlight colors
  886. * @param {Object | String} color
  887. * @return {Object} colorObject
  888. */
  889. util.parseColor = function(color) {
  890. var c;
  891. if (util.isString(color)) {
  892. if (util.isValidHex(color)) {
  893. var hsv = util.hexToHSV(color);
  894. var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
  895. var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
  896. var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
  897. var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
  898. c = {
  899. background: color,
  900. border:darkerColorHex,
  901. highlight: {
  902. background:lighterColorHex,
  903. border:darkerColorHex
  904. }
  905. };
  906. }
  907. else {
  908. c = {
  909. background:color,
  910. border:color,
  911. highlight: {
  912. background:color,
  913. border:color
  914. }
  915. };
  916. }
  917. }
  918. else {
  919. c = {};
  920. c.background = color.background || 'white';
  921. c.border = color.border || c.background;
  922. if (util.isString(color.highlight)) {
  923. c.highlight = {
  924. border: color.highlight,
  925. background: color.highlight
  926. }
  927. }
  928. else {
  929. c.highlight = {};
  930. c.highlight.background = color.highlight && color.highlight.background || c.background;
  931. c.highlight.border = color.highlight && color.highlight.border || c.border;
  932. }
  933. }
  934. return c;
  935. };
  936. /**
  937. * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
  938. *
  939. * @param {String} hex
  940. * @returns {{r: *, g: *, b: *}}
  941. */
  942. util.hexToRGB = function hexToRGB(hex) {
  943. hex = hex.replace("#","").toUpperCase();
  944. var a = util.GiveDec(hex.substring(0, 1));
  945. var b = util.GiveDec(hex.substring(1, 2));
  946. var c = util.GiveDec(hex.substring(2, 3));
  947. var d = util.GiveDec(hex.substring(3, 4));
  948. var e = util.GiveDec(hex.substring(4, 5));
  949. var f = util.GiveDec(hex.substring(5, 6));
  950. var r = (a * 16) + b;
  951. var g = (c * 16) + d;
  952. var b = (e * 16) + f;
  953. return {r:r,g:g,b:b};
  954. };
  955. util.RGBToHex = function RGBToHex(red,green,blue) {
  956. var a = util.GiveHex(Math.floor(red / 16));
  957. var b = util.GiveHex(red % 16);
  958. var c = util.GiveHex(Math.floor(green / 16));
  959. var d = util.GiveHex(green % 16);
  960. var e = util.GiveHex(Math.floor(blue / 16));
  961. var f = util.GiveHex(blue % 16);
  962. var hex = a + b + c + d + e + f;
  963. return "#" + hex;
  964. };
  965. /**
  966. * http://www.javascripter.net/faq/rgb2hsv.htm
  967. *
  968. * @param red
  969. * @param green
  970. * @param blue
  971. * @returns {*}
  972. * @constructor
  973. */
  974. util.RGBToHSV = function RGBToHSV (red,green,blue) {
  975. red=red/255; green=green/255; blue=blue/255;
  976. var minRGB = Math.min(red,Math.min(green,blue));
  977. var maxRGB = Math.max(red,Math.max(green,blue));
  978. // Black-gray-white
  979. if (minRGB == maxRGB) {
  980. return {h:0,s:0,v:minRGB};
  981. }
  982. // Colors other than black-gray-white:
  983. var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
  984. var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
  985. var hue = 60*(h - d/(maxRGB - minRGB))/360;
  986. var saturation = (maxRGB - minRGB)/maxRGB;
  987. var value = maxRGB;
  988. return {h:hue,s:saturation,v:value};
  989. };
  990. /**
  991. * https://gist.github.com/mjijackson/5311256
  992. * @param hue
  993. * @param saturation
  994. * @param value
  995. * @returns {{r: number, g: number, b: number}}
  996. * @constructor
  997. */
  998. util.HSVToRGB = function HSVToRGB(h, s, v) {
  999. var r, g, b;
  1000. var i = Math.floor(h * 6);
  1001. var f = h * 6 - i;
  1002. var p = v * (1 - s);
  1003. var q = v * (1 - f * s);
  1004. var t = v * (1 - (1 - f) * s);
  1005. switch (i % 6) {
  1006. case 0: r = v, g = t, b = p; break;
  1007. case 1: r = q, g = v, b = p; break;
  1008. case 2: r = p, g = v, b = t; break;
  1009. case 3: r = p, g = q, b = v; break;
  1010. case 4: r = t, g = p, b = v; break;
  1011. case 5: r = v, g = p, b = q; break;
  1012. }
  1013. return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
  1014. };
  1015. util.HSVToHex = function HSVToHex(h, s, v) {
  1016. var rgb = util.HSVToRGB(h, s, v);
  1017. return util.RGBToHex(rgb.r, rgb.g, rgb.b);
  1018. };
  1019. util.hexToHSV = function hexToHSV(hex) {
  1020. var rgb = util.hexToRGB(hex);
  1021. return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
  1022. };
  1023. util.isValidHex = function isValidHex(hex) {
  1024. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  1025. return isOk;
  1026. };
  1027. util.copyObject = function copyObject(objectFrom, objectTo) {
  1028. for (var i in objectFrom) {
  1029. if (objectFrom.hasOwnProperty(i)) {
  1030. if (typeof objectFrom[i] == "object") {
  1031. objectTo[i] = {};
  1032. util.copyObject(objectFrom[i], objectTo[i]);
  1033. }
  1034. else {
  1035. objectTo[i] = objectFrom[i];
  1036. }
  1037. }
  1038. }
  1039. };
  1040. /**
  1041. * DataSet
  1042. *
  1043. * Usage:
  1044. * var dataSet = new DataSet({
  1045. * fieldId: '_id',
  1046. * convert: {
  1047. * // ...
  1048. * }
  1049. * });
  1050. *
  1051. * dataSet.add(item);
  1052. * dataSet.add(data);
  1053. * dataSet.update(item);
  1054. * dataSet.update(data);
  1055. * dataSet.remove(id);
  1056. * dataSet.remove(ids);
  1057. * var data = dataSet.get();
  1058. * var data = dataSet.get(id);
  1059. * var data = dataSet.get(ids);
  1060. * var data = dataSet.get(ids, options, data);
  1061. * dataSet.clear();
  1062. *
  1063. * A data set can:
  1064. * - add/remove/update data
  1065. * - gives triggers upon changes in the data
  1066. * - can import/export data in various data formats
  1067. *
  1068. * @param {Object} [options] Available options:
  1069. * {String} fieldId Field name of the id in the
  1070. * items, 'id' by default.
  1071. * {Object.<String, String} convert
  1072. * A map with field names as key,
  1073. * and the field type as value.
  1074. * @constructor DataSet
  1075. */
  1076. // TODO: add a DataSet constructor DataSet(data, options)
  1077. function DataSet (options) {
  1078. this.id = util.randomUUID();
  1079. this.options = options || {};
  1080. this.data = {}; // map with data indexed by id
  1081. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1082. this.convert = {}; // field types by field name
  1083. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1084. if (this.options.convert) {
  1085. for (var field in this.options.convert) {
  1086. if (this.options.convert.hasOwnProperty(field)) {
  1087. var value = this.options.convert[field];
  1088. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1089. this.convert[field] = 'Date';
  1090. }
  1091. else {
  1092. this.convert[field] = value;
  1093. }
  1094. }
  1095. }
  1096. }
  1097. // event subscribers
  1098. this.subscribers = {};
  1099. this.internalIds = {}; // internally generated id's
  1100. }
  1101. /**
  1102. * Subscribe to an event, add an event listener
  1103. * @param {String} event Event name. Available events: 'put', 'update',
  1104. * 'remove'
  1105. * @param {function} callback Callback method. Called with three parameters:
  1106. * {String} event
  1107. * {Object | null} params
  1108. * {String | Number} senderId
  1109. */
  1110. DataSet.prototype.on = function on (event, callback) {
  1111. var subscribers = this.subscribers[event];
  1112. if (!subscribers) {
  1113. subscribers = [];
  1114. this.subscribers[event] = subscribers;
  1115. }
  1116. subscribers.push({
  1117. callback: callback
  1118. });
  1119. };
  1120. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1121. DataSet.prototype.subscribe = DataSet.prototype.on;
  1122. /**
  1123. * Unsubscribe from an event, remove an event listener
  1124. * @param {String} event
  1125. * @param {function} callback
  1126. */
  1127. DataSet.prototype.off = function off(event, callback) {
  1128. var subscribers = this.subscribers[event];
  1129. if (subscribers) {
  1130. this.subscribers[event] = subscribers.filter(function (listener) {
  1131. return (listener.callback != callback);
  1132. });
  1133. }
  1134. };
  1135. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1136. DataSet.prototype.unsubscribe = DataSet.prototype.off;
  1137. /**
  1138. * Trigger an event
  1139. * @param {String} event
  1140. * @param {Object | null} params
  1141. * @param {String} [senderId] Optional id of the sender.
  1142. * @private
  1143. */
  1144. DataSet.prototype._trigger = function (event, params, senderId) {
  1145. if (event == '*') {
  1146. throw new Error('Cannot trigger event *');
  1147. }
  1148. var subscribers = [];
  1149. if (event in this.subscribers) {
  1150. subscribers = subscribers.concat(this.subscribers[event]);
  1151. }
  1152. if ('*' in this.subscribers) {
  1153. subscribers = subscribers.concat(this.subscribers['*']);
  1154. }
  1155. for (var i = 0; i < subscribers.length; i++) {
  1156. var subscriber = subscribers[i];
  1157. if (subscriber.callback) {
  1158. subscriber.callback(event, params, senderId || null);
  1159. }
  1160. }
  1161. };
  1162. /**
  1163. * Add data.
  1164. * Adding an item will fail when there already is an item with the same id.
  1165. * @param {Object | Array | DataTable} data
  1166. * @param {String} [senderId] Optional sender id
  1167. * @return {Array} addedIds Array with the ids of the added items
  1168. */
  1169. DataSet.prototype.add = function (data, senderId) {
  1170. var addedIds = [],
  1171. id,
  1172. me = this;
  1173. if (data instanceof Array) {
  1174. // Array
  1175. for (var i = 0, len = data.length; i < len; i++) {
  1176. id = me._addItem(data[i]);
  1177. addedIds.push(id);
  1178. }
  1179. }
  1180. else if (util.isDataTable(data)) {
  1181. // Google DataTable
  1182. var columns = this._getColumnNames(data);
  1183. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1184. var item = {};
  1185. for (var col = 0, cols = columns.length; col < cols; col++) {
  1186. var field = columns[col];
  1187. item[field] = data.getValue(row, col);
  1188. }
  1189. id = me._addItem(item);
  1190. addedIds.push(id);
  1191. }
  1192. }
  1193. else if (data instanceof Object) {
  1194. // Single item
  1195. id = me._addItem(data);
  1196. addedIds.push(id);
  1197. }
  1198. else {
  1199. throw new Error('Unknown dataType');
  1200. }
  1201. if (addedIds.length) {
  1202. this._trigger('add', {items: addedIds}, senderId);
  1203. }
  1204. return addedIds;
  1205. };
  1206. /**
  1207. * Update existing items. When an item does not exist, it will be created
  1208. * @param {Object | Array | DataTable} data
  1209. * @param {String} [senderId] Optional sender id
  1210. * @return {Array} updatedIds The ids of the added or updated items
  1211. */
  1212. DataSet.prototype.update = function (data, senderId) {
  1213. var addedIds = [],
  1214. updatedIds = [],
  1215. me = this,
  1216. fieldId = me.fieldId;
  1217. var addOrUpdate = function (item) {
  1218. var id = item[fieldId];
  1219. if (me.data[id]) {
  1220. // update item
  1221. id = me._updateItem(item);
  1222. updatedIds.push(id);
  1223. }
  1224. else {
  1225. // add new item
  1226. id = me._addItem(item);
  1227. addedIds.push(id);
  1228. }
  1229. };
  1230. if (data instanceof Array) {
  1231. // Array
  1232. for (var i = 0, len = data.length; i < len; i++) {
  1233. addOrUpdate(data[i]);
  1234. }
  1235. }
  1236. else if (util.isDataTable(data)) {
  1237. // Google DataTable
  1238. var columns = this._getColumnNames(data);
  1239. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1240. var item = {};
  1241. for (var col = 0, cols = columns.length; col < cols; col++) {
  1242. var field = columns[col];
  1243. item[field] = data.getValue(row, col);
  1244. }
  1245. addOrUpdate(item);
  1246. }
  1247. }
  1248. else if (data instanceof Object) {
  1249. // Single item
  1250. addOrUpdate(data);
  1251. }
  1252. else {
  1253. throw new Error('Unknown dataType');
  1254. }
  1255. if (addedIds.length) {
  1256. this._trigger('add', {items: addedIds}, senderId);
  1257. }
  1258. if (updatedIds.length) {
  1259. this._trigger('update', {items: updatedIds}, senderId);
  1260. }
  1261. return addedIds.concat(updatedIds);
  1262. };
  1263. /**
  1264. * Get a data item or multiple items.
  1265. *
  1266. * Usage:
  1267. *
  1268. * get()
  1269. * get(options: Object)
  1270. * get(options: Object, data: Array | DataTable)
  1271. *
  1272. * get(id: Number | String)
  1273. * get(id: Number | String, options: Object)
  1274. * get(id: Number | String, options: Object, data: Array | DataTable)
  1275. *
  1276. * get(ids: Number[] | String[])
  1277. * get(ids: Number[] | String[], options: Object)
  1278. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1279. *
  1280. * Where:
  1281. *
  1282. * {Number | String} id The id of an item
  1283. * {Number[] | String{}} ids An array with ids of items
  1284. * {Object} options An Object with options. Available options:
  1285. * {String} [type] Type of data to be returned. Can
  1286. * be 'DataTable' or 'Array' (default)
  1287. * {Object.<String, String>} [convert]
  1288. * {String[]} [fields] field names to be returned
  1289. * {function} [filter] filter items
  1290. * {String | function} [order] Order the items by
  1291. * a field name or custom sort function.
  1292. * {Array | DataTable} [data] If provided, items will be appended to this
  1293. * array or table. Required in case of Google
  1294. * DataTable.
  1295. *
  1296. * @throws Error
  1297. */
  1298. DataSet.prototype.get = function (args) {
  1299. var me = this;
  1300. var globalShowInternalIds = this.showInternalIds;
  1301. // parse the arguments
  1302. var id, ids, options, data;
  1303. var firstType = util.getType(arguments[0]);
  1304. if (firstType == 'String' || firstType == 'Number') {
  1305. // get(id [, options] [, data])
  1306. id = arguments[0];
  1307. options = arguments[1];
  1308. data = arguments[2];
  1309. }
  1310. else if (firstType == 'Array') {
  1311. // get(ids [, options] [, data])
  1312. ids = arguments[0];
  1313. options = arguments[1];
  1314. data = arguments[2];
  1315. }
  1316. else {
  1317. // get([, options] [, data])
  1318. options = arguments[0];
  1319. data = arguments[1];
  1320. }
  1321. // determine the return type
  1322. var type;
  1323. if (options && options.type) {
  1324. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1325. if (data && (type != util.getType(data))) {
  1326. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1327. 'does not correspond with specified options.type (' + options.type + ')');
  1328. }
  1329. if (type == 'DataTable' && !util.isDataTable(data)) {
  1330. throw new Error('Parameter "data" must be a DataTable ' +
  1331. 'when options.type is "DataTable"');
  1332. }
  1333. }
  1334. else if (data) {
  1335. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1336. }
  1337. else {
  1338. type = 'Array';
  1339. }
  1340. // we allow the setting of this value for a single get request.
  1341. if (options != undefined) {
  1342. if (options.showInternalIds != undefined) {
  1343. this.showInternalIds = options.showInternalIds;
  1344. }
  1345. }
  1346. // build options
  1347. var convert = options && options.convert || this.options.convert;
  1348. var filter = options && options.filter;
  1349. var items = [], item, itemId, i, len;
  1350. // convert items
  1351. if (id != undefined) {
  1352. // return a single item
  1353. item = me._getItem(id, convert);
  1354. if (filter && !filter(item)) {
  1355. item = null;
  1356. }
  1357. }
  1358. else if (ids != undefined) {
  1359. // return a subset of items
  1360. for (i = 0, len = ids.length; i < len; i++) {
  1361. item = me._getItem(ids[i], convert);
  1362. if (!filter || filter(item)) {
  1363. items.push(item);
  1364. }
  1365. }
  1366. }
  1367. else {
  1368. // return all items
  1369. for (itemId in this.data) {
  1370. if (this.data.hasOwnProperty(itemId)) {
  1371. item = me._getItem(itemId, convert);
  1372. if (!filter || filter(item)) {
  1373. items.push(item);
  1374. }
  1375. }
  1376. }
  1377. }
  1378. // restore the global value of showInternalIds
  1379. this.showInternalIds = globalShowInternalIds;
  1380. // order the results
  1381. if (options && options.order && id == undefined) {
  1382. this._sort(items, options.order);
  1383. }
  1384. // filter fields of the items
  1385. if (options && options.fields) {
  1386. var fields = options.fields;
  1387. if (id != undefined) {
  1388. item = this._filterFields(item, fields);
  1389. }
  1390. else {
  1391. for (i = 0, len = items.length; i < len; i++) {
  1392. items[i] = this._filterFields(items[i], fields);
  1393. }
  1394. }
  1395. }
  1396. // return the results
  1397. if (type == 'DataTable') {
  1398. var columns = this._getColumnNames(data);
  1399. if (id != undefined) {
  1400. // append a single item to the data table
  1401. me._appendRow(data, columns, item);
  1402. }
  1403. else {
  1404. // copy the items to the provided data table
  1405. for (i = 0, len = items.length; i < len; i++) {
  1406. me._appendRow(data, columns, items[i]);
  1407. }
  1408. }
  1409. return data;
  1410. }
  1411. else {
  1412. // return an array
  1413. if (id != undefined) {
  1414. // a single item
  1415. return item;
  1416. }
  1417. else {
  1418. // multiple items
  1419. if (data) {
  1420. // copy the items to the provided array
  1421. for (i = 0, len = items.length; i < len; i++) {
  1422. data.push(items[i]);
  1423. }
  1424. return data;
  1425. }
  1426. else {
  1427. // just return our array
  1428. return items;
  1429. }
  1430. }
  1431. }
  1432. };
  1433. /**
  1434. * Get ids of all items or from a filtered set of items.
  1435. * @param {Object} [options] An Object with options. Available options:
  1436. * {function} [filter] filter items
  1437. * {String | function} [order] Order the items by
  1438. * a field name or custom sort function.
  1439. * @return {Array} ids
  1440. */
  1441. DataSet.prototype.getIds = function (options) {
  1442. var data = this.data,
  1443. filter = options && options.filter,
  1444. order = options && options.order,
  1445. convert = options && options.convert || this.options.convert,
  1446. i,
  1447. len,
  1448. id,
  1449. item,
  1450. items,
  1451. ids = [];
  1452. if (filter) {
  1453. // get filtered items
  1454. if (order) {
  1455. // create ordered list
  1456. items = [];
  1457. for (id in data) {
  1458. if (data.hasOwnProperty(id)) {
  1459. item = this._getItem(id, convert);
  1460. if (filter(item)) {
  1461. items.push(item);
  1462. }
  1463. }
  1464. }
  1465. this._sort(items, order);
  1466. for (i = 0, len = items.length; i < len; i++) {
  1467. ids[i] = items[i][this.fieldId];
  1468. }
  1469. }
  1470. else {
  1471. // create unordered list
  1472. for (id in data) {
  1473. if (data.hasOwnProperty(id)) {
  1474. item = this._getItem(id, convert);
  1475. if (filter(item)) {
  1476. ids.push(item[this.fieldId]);
  1477. }
  1478. }
  1479. }
  1480. }
  1481. }
  1482. else {
  1483. // get all items
  1484. if (order) {
  1485. // create an ordered list
  1486. items = [];
  1487. for (id in data) {
  1488. if (data.hasOwnProperty(id)) {
  1489. items.push(data[id]);
  1490. }
  1491. }
  1492. this._sort(items, order);
  1493. for (i = 0, len = items.length; i < len; i++) {
  1494. ids[i] = items[i][this.fieldId];
  1495. }
  1496. }
  1497. else {
  1498. // create unordered list
  1499. for (id in data) {
  1500. if (data.hasOwnProperty(id)) {
  1501. item = data[id];
  1502. ids.push(item[this.fieldId]);
  1503. }
  1504. }
  1505. }
  1506. }
  1507. return ids;
  1508. };
  1509. /**
  1510. * Execute a callback function for every item in the dataset.
  1511. * The order of the items is not determined.
  1512. * @param {function} callback
  1513. * @param {Object} [options] Available options:
  1514. * {Object.<String, String>} [convert]
  1515. * {String[]} [fields] filter fields
  1516. * {function} [filter] filter items
  1517. * {String | function} [order] Order the items by
  1518. * a field name or custom sort function.
  1519. */
  1520. DataSet.prototype.forEach = function (callback, options) {
  1521. var filter = options && options.filter,
  1522. convert = options && options.convert || this.options.convert,
  1523. data = this.data,
  1524. item,
  1525. id;
  1526. if (options && options.order) {
  1527. // execute forEach on ordered list
  1528. var items = this.get(options);
  1529. for (var i = 0, len = items.length; i < len; i++) {
  1530. item = items[i];
  1531. id = item[this.fieldId];
  1532. callback(item, id);
  1533. }
  1534. }
  1535. else {
  1536. // unordered
  1537. for (id in data) {
  1538. if (data.hasOwnProperty(id)) {
  1539. item = this._getItem(id, convert);
  1540. if (!filter || filter(item)) {
  1541. callback(item, id);
  1542. }
  1543. }
  1544. }
  1545. }
  1546. };
  1547. /**
  1548. * Map every item in the dataset.
  1549. * @param {function} callback
  1550. * @param {Object} [options] Available options:
  1551. * {Object.<String, String>} [convert]
  1552. * {String[]} [fields] filter fields
  1553. * {function} [filter] filter items
  1554. * {String | function} [order] Order the items by
  1555. * a field name or custom sort function.
  1556. * @return {Object[]} mappedItems
  1557. */
  1558. DataSet.prototype.map = function (callback, options) {
  1559. var filter = options && options.filter,
  1560. convert = options && options.convert || this.options.convert,
  1561. mappedItems = [],
  1562. data = this.data,
  1563. item;
  1564. // convert and filter items
  1565. for (var id in data) {
  1566. if (data.hasOwnProperty(id)) {
  1567. item = this._getItem(id, convert);
  1568. if (!filter || filter(item)) {
  1569. mappedItems.push(callback(item, id));
  1570. }
  1571. }
  1572. }
  1573. // order items
  1574. if (options && options.order) {
  1575. this._sort(mappedItems, options.order);
  1576. }
  1577. return mappedItems;
  1578. };
  1579. /**
  1580. * Filter the fields of an item
  1581. * @param {Object} item
  1582. * @param {String[]} fields Field names
  1583. * @return {Object} filteredItem
  1584. * @private
  1585. */
  1586. DataSet.prototype._filterFields = function (item, fields) {
  1587. var filteredItem = {};
  1588. for (var field in item) {
  1589. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1590. filteredItem[field] = item[field];
  1591. }
  1592. }
  1593. return filteredItem;
  1594. };
  1595. /**
  1596. * Sort the provided array with items
  1597. * @param {Object[]} items
  1598. * @param {String | function} order A field name or custom sort function.
  1599. * @private
  1600. */
  1601. DataSet.prototype._sort = function (items, order) {
  1602. if (util.isString(order)) {
  1603. // order by provided field name
  1604. var name = order; // field name
  1605. items.sort(function (a, b) {
  1606. var av = a[name];
  1607. var bv = b[name];
  1608. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1609. });
  1610. }
  1611. else if (typeof order === 'function') {
  1612. // order by sort function
  1613. items.sort(order);
  1614. }
  1615. // TODO: extend order by an Object {field:String, direction:String}
  1616. // where direction can be 'asc' or 'desc'
  1617. else {
  1618. throw new TypeError('Order must be a function or a string');
  1619. }
  1620. };
  1621. /**
  1622. * Remove an object by pointer or by id
  1623. * @param {String | Number | Object | Array} id Object or id, or an array with
  1624. * objects or ids to be removed
  1625. * @param {String} [senderId] Optional sender id
  1626. * @return {Array} removedIds
  1627. */
  1628. DataSet.prototype.remove = function (id, senderId) {
  1629. var removedIds = [],
  1630. i, len, removedId;
  1631. if (id instanceof Array) {
  1632. for (i = 0, len = id.length; i < len; i++) {
  1633. removedId = this._remove(id[i]);
  1634. if (removedId != null) {
  1635. removedIds.push(removedId);
  1636. }
  1637. }
  1638. }
  1639. else {
  1640. removedId = this._remove(id);
  1641. if (removedId != null) {
  1642. removedIds.push(removedId);
  1643. }
  1644. }
  1645. if (removedIds.length) {
  1646. this._trigger('remove', {items: removedIds}, senderId);
  1647. }
  1648. return removedIds;
  1649. };
  1650. /**
  1651. * Remove an item by its id
  1652. * @param {Number | String | Object} id id or item
  1653. * @returns {Number | String | null} id
  1654. * @private
  1655. */
  1656. DataSet.prototype._remove = function (id) {
  1657. if (util.isNumber(id) || util.isString(id)) {
  1658. if (this.data[id]) {
  1659. delete this.data[id];
  1660. delete this.internalIds[id];
  1661. return id;
  1662. }
  1663. }
  1664. else if (id instanceof Object) {
  1665. var itemId = id[this.fieldId];
  1666. if (itemId && this.data[itemId]) {
  1667. delete this.data[itemId];
  1668. delete this.internalIds[itemId];
  1669. return itemId;
  1670. }
  1671. }
  1672. return null;
  1673. };
  1674. /**
  1675. * Clear the data
  1676. * @param {String} [senderId] Optional sender id
  1677. * @return {Array} removedIds The ids of all removed items
  1678. */
  1679. DataSet.prototype.clear = function (senderId) {
  1680. var ids = Object.keys(this.data);
  1681. this.data = {};
  1682. this.internalIds = {};
  1683. this._trigger('remove', {items: ids}, senderId);
  1684. return ids;
  1685. };
  1686. /**
  1687. * Find the item with maximum value of a specified field
  1688. * @param {String} field
  1689. * @return {Object | null} item Item containing max value, or null if no items
  1690. */
  1691. DataSet.prototype.max = function (field) {
  1692. var data = this.data,
  1693. max = null,
  1694. maxField = null;
  1695. for (var id in data) {
  1696. if (data.hasOwnProperty(id)) {
  1697. var item = data[id];
  1698. var itemField = item[field];
  1699. if (itemField != null && (!max || itemField > maxField)) {
  1700. max = item;
  1701. maxField = itemField;
  1702. }
  1703. }
  1704. }
  1705. return max;
  1706. };
  1707. /**
  1708. * Find the item with minimum value of a specified field
  1709. * @param {String} field
  1710. * @return {Object | null} item Item containing max value, or null if no items
  1711. */
  1712. DataSet.prototype.min = function (field) {
  1713. var data = this.data,
  1714. min = null,
  1715. minField = null;
  1716. for (var id in data) {
  1717. if (data.hasOwnProperty(id)) {
  1718. var item = data[id];
  1719. var itemField = item[field];
  1720. if (itemField != null && (!min || itemField < minField)) {
  1721. min = item;
  1722. minField = itemField;
  1723. }
  1724. }
  1725. }
  1726. return min;
  1727. };
  1728. /**
  1729. * Find all distinct values of a specified field
  1730. * @param {String} field
  1731. * @return {Array} values Array containing all distinct values. If the data
  1732. * items do not contain the specified field, an array
  1733. * containing a single value undefined is returned.
  1734. * The returned array is unordered.
  1735. */
  1736. DataSet.prototype.distinct = function (field) {
  1737. var data = this.data,
  1738. values = [],
  1739. fieldType = this.options.convert[field],
  1740. count = 0;
  1741. for (var prop in data) {
  1742. if (data.hasOwnProperty(prop)) {
  1743. var item = data[prop];
  1744. var value = util.convert(item[field], fieldType);
  1745. var exists = false;
  1746. for (var i = 0; i < count; i++) {
  1747. if (values[i] == value) {
  1748. exists = true;
  1749. break;
  1750. }
  1751. }
  1752. if (!exists) {
  1753. values[count] = value;
  1754. count++;
  1755. }
  1756. }
  1757. }
  1758. return values;
  1759. };
  1760. /**
  1761. * Add a single item. Will fail when an item with the same id already exists.
  1762. * @param {Object} item
  1763. * @return {String} id
  1764. * @private
  1765. */
  1766. DataSet.prototype._addItem = function (item) {
  1767. var id = item[this.fieldId];
  1768. if (id != undefined) {
  1769. // check whether this id is already taken
  1770. if (this.data[id]) {
  1771. // item already exists
  1772. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1773. }
  1774. }
  1775. else {
  1776. // generate an id
  1777. id = util.randomUUID();
  1778. item[this.fieldId] = id;
  1779. this.internalIds[id] = item;
  1780. }
  1781. var d = {};
  1782. for (var field in item) {
  1783. if (item.hasOwnProperty(field)) {
  1784. var fieldType = this.convert[field]; // type may be undefined
  1785. d[field] = util.convert(item[field], fieldType);
  1786. }
  1787. }
  1788. this.data[id] = d;
  1789. return id;
  1790. };
  1791. /**
  1792. * Get an item. Fields can be converted to a specific type
  1793. * @param {String} id
  1794. * @param {Object.<String, String>} [convert] field types to convert
  1795. * @return {Object | null} item
  1796. * @private
  1797. */
  1798. DataSet.prototype._getItem = function (id, convert) {
  1799. var field, value;
  1800. // get the item from the dataset
  1801. var raw = this.data[id];
  1802. if (!raw) {
  1803. return null;
  1804. }
  1805. // convert the items field types
  1806. var converted = {},
  1807. fieldId = this.fieldId,
  1808. internalIds = this.internalIds;
  1809. if (convert) {
  1810. for (field in raw) {
  1811. if (raw.hasOwnProperty(field)) {
  1812. value = raw[field];
  1813. // output all fields, except internal ids
  1814. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1815. converted[field] = util.convert(value, convert[field]);
  1816. }
  1817. }
  1818. }
  1819. }
  1820. else {
  1821. // no field types specified, no converting needed
  1822. for (field in raw) {
  1823. if (raw.hasOwnProperty(field)) {
  1824. value = raw[field];
  1825. // output all fields, except internal ids
  1826. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1827. converted[field] = value;
  1828. }
  1829. }
  1830. }
  1831. }
  1832. return converted;
  1833. };
  1834. /**
  1835. * Update a single item: merge with existing item.
  1836. * Will fail when the item has no id, or when there does not exist an item
  1837. * with the same id.
  1838. * @param {Object} item
  1839. * @return {String} id
  1840. * @private
  1841. */
  1842. DataSet.prototype._updateItem = function (item) {
  1843. var id = item[this.fieldId];
  1844. if (id == undefined) {
  1845. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1846. }
  1847. var d = this.data[id];
  1848. if (!d) {
  1849. // item doesn't exist
  1850. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1851. }
  1852. // merge with current item
  1853. for (var field in item) {
  1854. if (item.hasOwnProperty(field)) {
  1855. var fieldType = this.convert[field]; // type may be undefined
  1856. d[field] = util.convert(item[field], fieldType);
  1857. }
  1858. }
  1859. return id;
  1860. };
  1861. /**
  1862. * check if an id is an internal or external id
  1863. * @param id
  1864. * @returns {boolean}
  1865. * @private
  1866. */
  1867. DataSet.prototype.isInternalId = function(id) {
  1868. return (id in this.internalIds);
  1869. };
  1870. /**
  1871. * Get an array with the column names of a Google DataTable
  1872. * @param {DataTable} dataTable
  1873. * @return {String[]} columnNames
  1874. * @private
  1875. */
  1876. DataSet.prototype._getColumnNames = function (dataTable) {
  1877. var columns = [];
  1878. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1879. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1880. }
  1881. return columns;
  1882. };
  1883. /**
  1884. * Append an item as a row to the dataTable
  1885. * @param dataTable
  1886. * @param columns
  1887. * @param item
  1888. * @private
  1889. */
  1890. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1891. var row = dataTable.addRow();
  1892. for (var col = 0, cols = columns.length; col < cols; col++) {
  1893. var field = columns[col];
  1894. dataTable.setValue(row, col, item[field]);
  1895. }
  1896. };
  1897. /**
  1898. * DataView
  1899. *
  1900. * a dataview offers a filtered view on a dataset or an other dataview.
  1901. *
  1902. * @param {DataSet | DataView} data
  1903. * @param {Object} [options] Available options: see method get
  1904. *
  1905. * @constructor DataView
  1906. */
  1907. function DataView (data, options) {
  1908. this.id = util.randomUUID();
  1909. this.data = null;
  1910. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1911. this.options = options || {};
  1912. this.fieldId = 'id'; // name of the field containing id
  1913. this.subscribers = {}; // event subscribers
  1914. var me = this;
  1915. this.listener = function () {
  1916. me._onEvent.apply(me, arguments);
  1917. };
  1918. this.setData(data);
  1919. }
  1920. // TODO: implement a function .config() to dynamically update things like configured filter
  1921. // and trigger changes accordingly
  1922. /**
  1923. * Set a data source for the view
  1924. * @param {DataSet | DataView} data
  1925. */
  1926. DataView.prototype.setData = function (data) {
  1927. var ids, dataItems, i, len;
  1928. if (this.data) {
  1929. // unsubscribe from current dataset
  1930. if (this.data.unsubscribe) {
  1931. this.data.unsubscribe('*', this.listener);
  1932. }
  1933. // trigger a remove of all items in memory
  1934. ids = [];
  1935. for (var id in this.ids) {
  1936. if (this.ids.hasOwnProperty(id)) {
  1937. ids.push(id);
  1938. }
  1939. }
  1940. this.ids = {};
  1941. this._trigger('remove', {items: ids});
  1942. }
  1943. this.data = data;
  1944. if (this.data) {
  1945. // update fieldId
  1946. this.fieldId = this.options.fieldId ||
  1947. (this.data && this.data.options && this.data.options.fieldId) ||
  1948. 'id';
  1949. // trigger an add of all added items
  1950. ids = this.data.getIds({filter: this.options && this.options.filter});
  1951. for (i = 0, len = ids.length; i < len; i++) {
  1952. id = ids[i];
  1953. this.ids[id] = true;
  1954. }
  1955. this._trigger('add', {items: ids});
  1956. // subscribe to new dataset
  1957. if (this.data.on) {
  1958. this.data.on('*', this.listener);
  1959. }
  1960. }
  1961. };
  1962. /**
  1963. * Get data from the data view
  1964. *
  1965. * Usage:
  1966. *
  1967. * get()
  1968. * get(options: Object)
  1969. * get(options: Object, data: Array | DataTable)
  1970. *
  1971. * get(id: Number)
  1972. * get(id: Number, options: Object)
  1973. * get(id: Number, options: Object, data: Array | DataTable)
  1974. *
  1975. * get(ids: Number[])
  1976. * get(ids: Number[], options: Object)
  1977. * get(ids: Number[], options: Object, data: Array | DataTable)
  1978. *
  1979. * Where:
  1980. *
  1981. * {Number | String} id The id of an item
  1982. * {Number[] | String{}} ids An array with ids of items
  1983. * {Object} options An Object with options. Available options:
  1984. * {String} [type] Type of data to be returned. Can
  1985. * be 'DataTable' or 'Array' (default)
  1986. * {Object.<String, String>} [convert]
  1987. * {String[]} [fields] field names to be returned
  1988. * {function} [filter] filter items
  1989. * {String | function} [order] Order the items by
  1990. * a field name or custom sort function.
  1991. * {Array | DataTable} [data] If provided, items will be appended to this
  1992. * array or table. Required in case of Google
  1993. * DataTable.
  1994. * @param args
  1995. */
  1996. DataView.prototype.get = function (args) {
  1997. var me = this;
  1998. // parse the arguments
  1999. var ids, options, data;
  2000. var firstType = util.getType(arguments[0]);
  2001. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2002. // get(id(s) [, options] [, data])
  2003. ids = arguments[0]; // can be a single id or an array with ids
  2004. options = arguments[1];
  2005. data = arguments[2];
  2006. }
  2007. else {
  2008. // get([, options] [, data])
  2009. options = arguments[0];
  2010. data = arguments[1];
  2011. }
  2012. // extend the options with the default options and provided options
  2013. var viewOptions = util.extend({}, this.options, options);
  2014. // create a combined filter method when needed
  2015. if (this.options.filter && options && options.filter) {
  2016. viewOptions.filter = function (item) {
  2017. return me.options.filter(item) && options.filter(item);
  2018. }
  2019. }
  2020. // build up the call to the linked data set
  2021. var getArguments = [];
  2022. if (ids != undefined) {
  2023. getArguments.push(ids);
  2024. }
  2025. getArguments.push(viewOptions);
  2026. getArguments.push(data);
  2027. return this.data && this.data.get.apply(this.data, getArguments);
  2028. };
  2029. /**
  2030. * Get ids of all items or from a filtered set of items.
  2031. * @param {Object} [options] An Object with options. Available options:
  2032. * {function} [filter] filter items
  2033. * {String | function} [order] Order the items by
  2034. * a field name or custom sort function.
  2035. * @return {Array} ids
  2036. */
  2037. DataView.prototype.getIds = function (options) {
  2038. var ids;
  2039. if (this.data) {
  2040. var defaultFilter = this.options.filter;
  2041. var filter;
  2042. if (options && options.filter) {
  2043. if (defaultFilter) {
  2044. filter = function (item) {
  2045. return defaultFilter(item) && options.filter(item);
  2046. }
  2047. }
  2048. else {
  2049. filter = options.filter;
  2050. }
  2051. }
  2052. else {
  2053. filter = defaultFilter;
  2054. }
  2055. ids = this.data.getIds({
  2056. filter: filter,
  2057. order: options && options.order
  2058. });
  2059. }
  2060. else {
  2061. ids = [];
  2062. }
  2063. return ids;
  2064. };
  2065. /**
  2066. * Event listener. Will propagate all events from the connected data set to
  2067. * the subscribers of the DataView, but will filter the items and only trigger
  2068. * when there are changes in the filtered data set.
  2069. * @param {String} event
  2070. * @param {Object | null} params
  2071. * @param {String} senderId
  2072. * @private
  2073. */
  2074. DataView.prototype._onEvent = function (event, params, senderId) {
  2075. var i, len, id, item,
  2076. ids = params && params.items,
  2077. data = this.data,
  2078. added = [],
  2079. updated = [],
  2080. removed = [];
  2081. if (ids && data) {
  2082. switch (event) {
  2083. case 'add':
  2084. // filter the ids of the added items
  2085. for (i = 0, len = ids.length; i < len; i++) {
  2086. id = ids[i];
  2087. item = this.get(id);
  2088. if (item) {
  2089. this.ids[id] = true;
  2090. added.push(id);
  2091. }
  2092. }
  2093. break;
  2094. case 'update':
  2095. // determine the event from the views viewpoint: an updated
  2096. // item can be added, updated, or removed from this view.
  2097. for (i = 0, len = ids.length; i < len; i++) {
  2098. id = ids[i];
  2099. item = this.get(id);
  2100. if (item) {
  2101. if (this.ids[id]) {
  2102. updated.push(id);
  2103. }
  2104. else {
  2105. this.ids[id] = true;
  2106. added.push(id);
  2107. }
  2108. }
  2109. else {
  2110. if (this.ids[id]) {
  2111. delete this.ids[id];
  2112. removed.push(id);
  2113. }
  2114. else {
  2115. // nothing interesting for me :-(
  2116. }
  2117. }
  2118. }
  2119. break;
  2120. case 'remove':
  2121. // filter the ids of the removed items
  2122. for (i = 0, len = ids.length; i < len; i++) {
  2123. id = ids[i];
  2124. if (this.ids[id]) {
  2125. delete this.ids[id];
  2126. removed.push(id);
  2127. }
  2128. }
  2129. break;
  2130. }
  2131. if (added.length) {
  2132. this._trigger('add', {items: added}, senderId);
  2133. }
  2134. if (updated.length) {
  2135. this._trigger('update', {items: updated}, senderId);
  2136. }
  2137. if (removed.length) {
  2138. this._trigger('remove', {items: removed}, senderId);
  2139. }
  2140. }
  2141. };
  2142. // copy subscription functionality from DataSet
  2143. DataView.prototype.on = DataSet.prototype.on;
  2144. DataView.prototype.off = DataSet.prototype.off;
  2145. DataView.prototype._trigger = DataSet.prototype._trigger;
  2146. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2147. DataView.prototype.subscribe = DataView.prototype.on;
  2148. DataView.prototype.unsubscribe = DataView.prototype.off;
  2149. /**
  2150. * @constructor TimeStep
  2151. * The class TimeStep is an iterator for dates. You provide a start date and an
  2152. * end date. The class itself determines the best scale (step size) based on the
  2153. * provided start Date, end Date, and minimumStep.
  2154. *
  2155. * If minimumStep is provided, the step size is chosen as close as possible
  2156. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2157. * provided, the scale is set to 1 DAY.
  2158. * The minimumStep should correspond with the onscreen size of about 6 characters
  2159. *
  2160. * Alternatively, you can set a scale by hand.
  2161. * After creation, you can initialize the class by executing first(). Then you
  2162. * can iterate from the start date to the end date via next(). You can check if
  2163. * the end date is reached with the function hasNext(). After each step, you can
  2164. * retrieve the current date via getCurrent().
  2165. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2166. * days, to years.
  2167. *
  2168. * Version: 1.2
  2169. *
  2170. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2171. * or new Date(2010, 9, 21, 23, 45, 00)
  2172. * @param {Date} [end] The end date
  2173. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2174. */
  2175. TimeStep = function(start, end, minimumStep) {
  2176. // variables
  2177. this.current = new Date();
  2178. this._start = new Date();
  2179. this._end = new Date();
  2180. this.autoScale = true;
  2181. this.scale = TimeStep.SCALE.DAY;
  2182. this.step = 1;
  2183. // initialize the range
  2184. this.setRange(start, end, minimumStep);
  2185. };
  2186. /// enum scale
  2187. TimeStep.SCALE = {
  2188. MILLISECOND: 1,
  2189. SECOND: 2,
  2190. MINUTE: 3,
  2191. HOUR: 4,
  2192. DAY: 5,
  2193. WEEKDAY: 6,
  2194. MONTH: 7,
  2195. YEAR: 8
  2196. };
  2197. /**
  2198. * Set a new range
  2199. * If minimumStep is provided, the step size is chosen as close as possible
  2200. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2201. * provided, the scale is set to 1 DAY.
  2202. * The minimumStep should correspond with the onscreen size of about 6 characters
  2203. * @param {Date} [start] The start date and time.
  2204. * @param {Date} [end] The end date and time.
  2205. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2206. */
  2207. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2208. if (!(start instanceof Date) || !(end instanceof Date)) {
  2209. throw "No legal start or end date in method setRange";
  2210. }
  2211. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2212. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2213. if (this.autoScale) {
  2214. this.setMinimumStep(minimumStep);
  2215. }
  2216. };
  2217. /**
  2218. * Set the range iterator to the start date.
  2219. */
  2220. TimeStep.prototype.first = function() {
  2221. this.current = new Date(this._start.valueOf());
  2222. this.roundToMinor();
  2223. };
  2224. /**
  2225. * Round the current date to the first minor date value
  2226. * This must be executed once when the current date is set to start Date
  2227. */
  2228. TimeStep.prototype.roundToMinor = function() {
  2229. // round to floor
  2230. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2231. //noinspection FallthroughInSwitchStatementJS
  2232. switch (this.scale) {
  2233. case TimeStep.SCALE.YEAR:
  2234. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2235. this.current.setMonth(0);
  2236. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2237. case TimeStep.SCALE.DAY: // intentional fall through
  2238. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2239. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2240. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2241. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2242. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2243. }
  2244. if (this.step != 1) {
  2245. // round down to the first minor value that is a multiple of the current step size
  2246. switch (this.scale) {
  2247. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2248. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2249. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2250. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2251. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2252. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2253. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2254. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2255. default: break;
  2256. }
  2257. }
  2258. };
  2259. /**
  2260. * Check if the there is a next step
  2261. * @return {boolean} true if the current date has not passed the end date
  2262. */
  2263. TimeStep.prototype.hasNext = function () {
  2264. return (this.current.valueOf() <= this._end.valueOf());
  2265. };
  2266. /**
  2267. * Do the next step
  2268. */
  2269. TimeStep.prototype.next = function() {
  2270. var prev = this.current.valueOf();
  2271. // Two cases, needed to prevent issues with switching daylight savings
  2272. // (end of March and end of October)
  2273. if (this.current.getMonth() < 6) {
  2274. switch (this.scale) {
  2275. case TimeStep.SCALE.MILLISECOND:
  2276. this.current = new Date(this.current.valueOf() + this.step); break;
  2277. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2278. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2279. case TimeStep.SCALE.HOUR:
  2280. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2281. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2282. var h = this.current.getHours();
  2283. this.current.setHours(h - (h % this.step));
  2284. break;
  2285. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2286. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2287. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2288. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2289. default: break;
  2290. }
  2291. }
  2292. else {
  2293. switch (this.scale) {
  2294. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2295. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2296. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2297. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2298. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2299. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2300. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2301. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2302. default: break;
  2303. }
  2304. }
  2305. if (this.step != 1) {
  2306. // round down to the correct major value
  2307. switch (this.scale) {
  2308. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2309. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2310. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2311. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2312. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2313. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2314. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2315. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2316. default: break;
  2317. }
  2318. }
  2319. // safety mechanism: if current time is still unchanged, move to the end
  2320. if (this.current.valueOf() == prev) {
  2321. this.current = new Date(this._end.valueOf());
  2322. }
  2323. };
  2324. /**
  2325. * Get the current datetime
  2326. * @return {Date} current The current date
  2327. */
  2328. TimeStep.prototype.getCurrent = function() {
  2329. return this.current;
  2330. };
  2331. /**
  2332. * Set a custom scale. Autoscaling will be disabled.
  2333. * For example setScale(SCALE.MINUTES, 5) will result
  2334. * in minor steps of 5 minutes, and major steps of an hour.
  2335. *
  2336. * @param {TimeStep.SCALE} newScale
  2337. * A scale. Choose from SCALE.MILLISECOND,
  2338. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2339. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2340. * SCALE.YEAR.
  2341. * @param {Number} newStep A step size, by default 1. Choose for
  2342. * example 1, 2, 5, or 10.
  2343. */
  2344. TimeStep.prototype.setScale = function(newScale, newStep) {
  2345. this.scale = newScale;
  2346. if (newStep > 0) {
  2347. this.step = newStep;
  2348. }
  2349. this.autoScale = false;
  2350. };
  2351. /**
  2352. * Enable or disable autoscaling
  2353. * @param {boolean} enable If true, autoascaling is set true
  2354. */
  2355. TimeStep.prototype.setAutoScale = function (enable) {
  2356. this.autoScale = enable;
  2357. };
  2358. /**
  2359. * Automatically determine the scale that bests fits the provided minimum step
  2360. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2361. */
  2362. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2363. if (minimumStep == undefined) {
  2364. return;
  2365. }
  2366. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2367. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2368. var stepDay = (1000 * 60 * 60 * 24);
  2369. var stepHour = (1000 * 60 * 60);
  2370. var stepMinute = (1000 * 60);
  2371. var stepSecond = (1000);
  2372. var stepMillisecond= (1);
  2373. // find the smallest step that is larger than the provided minimumStep
  2374. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2375. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2376. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2377. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2378. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2379. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2380. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2381. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2382. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2383. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2384. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2385. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2386. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2387. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2388. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2389. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2390. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2391. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2392. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2393. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2394. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2395. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2396. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2397. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2398. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2399. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2400. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2401. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2402. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2403. };
  2404. /**
  2405. * Snap a date to a rounded value.
  2406. * The snap intervals are dependent on the current scale and step.
  2407. * @param {Date} date the date to be snapped.
  2408. * @return {Date} snappedDate
  2409. */
  2410. TimeStep.prototype.snap = function(date) {
  2411. var clone = new Date(date.valueOf());
  2412. if (this.scale == TimeStep.SCALE.YEAR) {
  2413. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  2414. clone.setFullYear(Math.round(year / this.step) * this.step);
  2415. clone.setMonth(0);
  2416. clone.setDate(0);
  2417. clone.setHours(0);
  2418. clone.setMinutes(0);
  2419. clone.setSeconds(0);
  2420. clone.setMilliseconds(0);
  2421. }
  2422. else if (this.scale == TimeStep.SCALE.MONTH) {
  2423. if (clone.getDate() > 15) {
  2424. clone.setDate(1);
  2425. clone.setMonth(clone.getMonth() + 1);
  2426. // important: first set Date to 1, after that change the month.
  2427. }
  2428. else {
  2429. clone.setDate(1);
  2430. }
  2431. clone.setHours(0);
  2432. clone.setMinutes(0);
  2433. clone.setSeconds(0);
  2434. clone.setMilliseconds(0);
  2435. }
  2436. else if (this.scale == TimeStep.SCALE.DAY ||
  2437. this.scale == TimeStep.SCALE.WEEKDAY) {
  2438. //noinspection FallthroughInSwitchStatementJS
  2439. switch (this.step) {
  2440. case 5:
  2441. case 2:
  2442. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  2443. default:
  2444. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2445. }
  2446. clone.setMinutes(0);
  2447. clone.setSeconds(0);
  2448. clone.setMilliseconds(0);
  2449. }
  2450. else if (this.scale == TimeStep.SCALE.HOUR) {
  2451. switch (this.step) {
  2452. case 4:
  2453. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  2454. default:
  2455. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  2456. }
  2457. clone.setSeconds(0);
  2458. clone.setMilliseconds(0);
  2459. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2460. //noinspection FallthroughInSwitchStatementJS
  2461. switch (this.step) {
  2462. case 15:
  2463. case 10:
  2464. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  2465. clone.setSeconds(0);
  2466. break;
  2467. case 5:
  2468. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  2469. default:
  2470. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  2471. }
  2472. clone.setMilliseconds(0);
  2473. }
  2474. else if (this.scale == TimeStep.SCALE.SECOND) {
  2475. //noinspection FallthroughInSwitchStatementJS
  2476. switch (this.step) {
  2477. case 15:
  2478. case 10:
  2479. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  2480. clone.setMilliseconds(0);
  2481. break;
  2482. case 5:
  2483. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  2484. default:
  2485. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  2486. }
  2487. }
  2488. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2489. var step = this.step > 5 ? this.step / 2 : 1;
  2490. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  2491. }
  2492. return clone;
  2493. };
  2494. /**
  2495. * Check if the current value is a major value (for example when the step
  2496. * is DAY, a major value is each first day of the MONTH)
  2497. * @return {boolean} true if current date is major, else false.
  2498. */
  2499. TimeStep.prototype.isMajor = function() {
  2500. switch (this.scale) {
  2501. case TimeStep.SCALE.MILLISECOND:
  2502. return (this.current.getMilliseconds() == 0);
  2503. case TimeStep.SCALE.SECOND:
  2504. return (this.current.getSeconds() == 0);
  2505. case TimeStep.SCALE.MINUTE:
  2506. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2507. // Note: this is no bug. Major label is equal for both minute and hour scale
  2508. case TimeStep.SCALE.HOUR:
  2509. return (this.current.getHours() == 0);
  2510. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2511. case TimeStep.SCALE.DAY:
  2512. return (this.current.getDate() == 1);
  2513. case TimeStep.SCALE.MONTH:
  2514. return (this.current.getMonth() == 0);
  2515. case TimeStep.SCALE.YEAR:
  2516. return false;
  2517. default:
  2518. return false;
  2519. }
  2520. };
  2521. /**
  2522. * Returns formatted text for the minor axislabel, depending on the current
  2523. * date and the scale. For example when scale is MINUTE, the current time is
  2524. * formatted as "hh:mm".
  2525. * @param {Date} [date] custom date. if not provided, current date is taken
  2526. */
  2527. TimeStep.prototype.getLabelMinor = function(date) {
  2528. if (date == undefined) {
  2529. date = this.current;
  2530. }
  2531. switch (this.scale) {
  2532. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2533. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2534. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2535. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2536. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2537. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2538. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2539. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2540. default: return '';
  2541. }
  2542. };
  2543. /**
  2544. * Returns formatted text for the major axis label, depending on the current
  2545. * date and the scale. For example when scale is MINUTE, the major scale is
  2546. * hours, and the hour will be formatted as "hh".
  2547. * @param {Date} [date] custom date. if not provided, current date is taken
  2548. */
  2549. TimeStep.prototype.getLabelMajor = function(date) {
  2550. if (date == undefined) {
  2551. date = this.current;
  2552. }
  2553. //noinspection FallthroughInSwitchStatementJS
  2554. switch (this.scale) {
  2555. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2556. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2557. case TimeStep.SCALE.MINUTE:
  2558. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2559. case TimeStep.SCALE.WEEKDAY:
  2560. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2561. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2562. case TimeStep.SCALE.YEAR: return '';
  2563. default: return '';
  2564. }
  2565. };
  2566. /**
  2567. * @constructor Stack
  2568. * Stacks items on top of each other.
  2569. * @param {ItemSet} itemset
  2570. * @param {Object} [options]
  2571. */
  2572. function Stack (itemset, options) {
  2573. this.itemset = itemset;
  2574. this.options = options || {};
  2575. this.defaultOptions = {
  2576. order: function (a, b) {
  2577. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2578. // Order: ranges over non-ranges, ranged ordered by width, and
  2579. // lastly ordered by start.
  2580. if (a instanceof ItemRange) {
  2581. if (b instanceof ItemRange) {
  2582. var aInt = (a.data.end - a.data.start);
  2583. var bInt = (b.data.end - b.data.start);
  2584. return (aInt - bInt) || (a.data.start - b.data.start);
  2585. }
  2586. else {
  2587. return -1;
  2588. }
  2589. }
  2590. else {
  2591. if (b instanceof ItemRange) {
  2592. return 1;
  2593. }
  2594. else {
  2595. return (a.data.start - b.data.start);
  2596. }
  2597. }
  2598. },
  2599. margin: {
  2600. item: 10
  2601. }
  2602. };
  2603. this.ordered = []; // ordered items
  2604. }
  2605. /**
  2606. * Set options for the stack
  2607. * @param {Object} options Available options:
  2608. * {ItemSet} itemset
  2609. * {Number} margin
  2610. * {function} order Stacking order
  2611. */
  2612. Stack.prototype.setOptions = function setOptions (options) {
  2613. util.extend(this.options, options);
  2614. // TODO: register on data changes at the connected itemset, and update the changed part only and immediately
  2615. };
  2616. /**
  2617. * Stack the items such that they don't overlap. The items will have a minimal
  2618. * distance equal to options.margin.item.
  2619. */
  2620. Stack.prototype.update = function update() {
  2621. this._order();
  2622. this._stack();
  2623. };
  2624. /**
  2625. * Order the items. If a custom order function has been provided via the options,
  2626. * then this will be used.
  2627. * @private
  2628. */
  2629. Stack.prototype._order = function _order () {
  2630. var items = this.itemset.items;
  2631. if (!items) {
  2632. throw new Error('Cannot stack items: ItemSet does not contain items');
  2633. }
  2634. // TODO: store the sorted items, to have less work later on
  2635. var ordered = [];
  2636. var index = 0;
  2637. // items is a map (no array)
  2638. util.forEach(items, function (item) {
  2639. if (item.visible) {
  2640. ordered[index] = item;
  2641. index++;
  2642. }
  2643. });
  2644. //if a customer stack order function exists, use it.
  2645. var order = this.options.order || this.defaultOptions.order;
  2646. if (!(typeof order === 'function')) {
  2647. throw new Error('Option order must be a function');
  2648. }
  2649. ordered.sort(order);
  2650. this.ordered = ordered;
  2651. };
  2652. /**
  2653. * Adjust vertical positions of the events such that they don't overlap each
  2654. * other.
  2655. * @private
  2656. */
  2657. Stack.prototype._stack = function _stack () {
  2658. var i,
  2659. iMax,
  2660. ordered = this.ordered,
  2661. options = this.options,
  2662. orientation = options.orientation || this.defaultOptions.orientation,
  2663. axisOnTop = (orientation == 'top'),
  2664. margin;
  2665. if (options.margin && options.margin.item !== undefined) {
  2666. margin = options.margin.item;
  2667. }
  2668. else {
  2669. margin = this.defaultOptions.margin.item
  2670. }
  2671. // calculate new, non-overlapping positions
  2672. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2673. var item = ordered[i];
  2674. var collidingItem = null;
  2675. do {
  2676. // TODO: optimize checking for overlap. when there is a gap without items,
  2677. // you only need to check for items from the next item on, not from zero
  2678. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2679. if (collidingItem != null) {
  2680. // There is a collision. Reposition the event above the colliding element
  2681. if (axisOnTop) {
  2682. item.top = collidingItem.top + collidingItem.height + margin;
  2683. }
  2684. else {
  2685. item.top = collidingItem.top - item.height - margin;
  2686. }
  2687. }
  2688. } while (collidingItem);
  2689. }
  2690. };
  2691. /**
  2692. * Check if the destiny position of given item overlaps with any
  2693. * of the other items from index itemStart to itemEnd.
  2694. * @param {Array} items Array with items
  2695. * @param {int} itemIndex Number of the item to be checked for overlap
  2696. * @param {int} itemStart First item to be checked.
  2697. * @param {int} itemEnd Last item to be checked.
  2698. * @return {Object | null} colliding item, or undefined when no collisions
  2699. * @param {Number} margin A minimum required margin.
  2700. * If margin is provided, the two items will be
  2701. * marked colliding when they overlap or
  2702. * when the margin between the two is smaller than
  2703. * the requested margin.
  2704. */
  2705. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2706. itemStart, itemEnd, margin) {
  2707. var collision = this.collision;
  2708. // we loop from end to start, as we suppose that the chance of a
  2709. // collision is larger for items at the end, so check these first.
  2710. var a = items[itemIndex];
  2711. for (var i = itemEnd; i >= itemStart; i--) {
  2712. var b = items[i];
  2713. if (collision(a, b, margin)) {
  2714. if (i != itemIndex) {
  2715. return b;
  2716. }
  2717. }
  2718. }
  2719. return null;
  2720. };
  2721. /**
  2722. * Test if the two provided items collide
  2723. * The items must have parameters left, width, top, and height.
  2724. * @param {Component} a The first item
  2725. * @param {Component} b The second item
  2726. * @param {Number} margin A minimum required margin.
  2727. * If margin is provided, the two items will be
  2728. * marked colliding when they overlap or
  2729. * when the margin between the two is smaller than
  2730. * the requested margin.
  2731. * @return {boolean} true if a and b collide, else false
  2732. */
  2733. Stack.prototype.collision = function collision (a, b, margin) {
  2734. return ((a.left - margin) < (b.left + b.width) &&
  2735. (a.left + a.width + margin) > b.left &&
  2736. (a.top - margin) < (b.top + b.height) &&
  2737. (a.top + a.height + margin) > b.top);
  2738. };
  2739. /**
  2740. * @constructor Range
  2741. * A Range controls a numeric range with a start and end value.
  2742. * The Range adjusts the range based on mouse events or programmatic changes,
  2743. * and triggers events when the range is changing or has been changed.
  2744. * @param {Object} [options] See description at Range.setOptions
  2745. * @extends Controller
  2746. */
  2747. function Range(options) {
  2748. this.id = util.randomUUID();
  2749. this.start = null; // Number
  2750. this.end = null; // Number
  2751. this.options = options || {};
  2752. this.setOptions(options);
  2753. }
  2754. // extend the Range prototype with an event emitter mixin
  2755. Emitter(Range.prototype);
  2756. /**
  2757. * Set options for the range controller
  2758. * @param {Object} options Available options:
  2759. * {Number} min Minimum value for start
  2760. * {Number} max Maximum value for end
  2761. * {Number} zoomMin Set a minimum value for
  2762. * (end - start).
  2763. * {Number} zoomMax Set a maximum value for
  2764. * (end - start).
  2765. */
  2766. Range.prototype.setOptions = function (options) {
  2767. util.extend(this.options, options);
  2768. // re-apply range with new limitations
  2769. if (this.start !== null && this.end !== null) {
  2770. this.setRange(this.start, this.end);
  2771. }
  2772. };
  2773. /**
  2774. * Test whether direction has a valid value
  2775. * @param {String} direction 'horizontal' or 'vertical'
  2776. */
  2777. function validateDirection (direction) {
  2778. if (direction != 'horizontal' && direction != 'vertical') {
  2779. throw new TypeError('Unknown direction "' + direction + '". ' +
  2780. 'Choose "horizontal" or "vertical".');
  2781. }
  2782. }
  2783. /**
  2784. * Add listeners for mouse and touch events to the component
  2785. * @param {Controller} controller
  2786. * @param {Component} component Should be a rootpanel
  2787. * @param {String} event Available events: 'move', 'zoom'
  2788. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2789. */
  2790. Range.prototype.subscribe = function (controller, component, event, direction) {
  2791. var me = this;
  2792. if (event == 'move') {
  2793. // drag start listener
  2794. controller.on('dragstart', function (event) {
  2795. me._onDragStart(event, component);
  2796. });
  2797. // drag listener
  2798. controller.on('drag', function (event) {
  2799. me._onDrag(event, component, direction);
  2800. });
  2801. // drag end listener
  2802. controller.on('dragend', function (event) {
  2803. me._onDragEnd(event, component);
  2804. });
  2805. // ignore dragging when holding
  2806. controller.on('hold', function (event) {
  2807. me._onHold();
  2808. });
  2809. }
  2810. else if (event == 'zoom') {
  2811. // mouse wheel
  2812. function mousewheel (event) {
  2813. me._onMouseWheel(event, component, direction);
  2814. }
  2815. controller.on('mousewheel', mousewheel);
  2816. controller.on('DOMMouseScroll', mousewheel); // For FF
  2817. // pinch
  2818. controller.on('touch', function (event) {
  2819. me._onTouch(event);
  2820. });
  2821. controller.on('pinch', function (event) {
  2822. me._onPinch(event, component, direction);
  2823. });
  2824. }
  2825. else {
  2826. throw new TypeError('Unknown event "' + event + '". ' +
  2827. 'Choose "move" or "zoom".');
  2828. }
  2829. };
  2830. /**
  2831. * Set a new start and end range
  2832. * @param {Number} [start]
  2833. * @param {Number} [end]
  2834. */
  2835. Range.prototype.setRange = function(start, end) {
  2836. var changed = this._applyRange(start, end);
  2837. if (changed) {
  2838. var params = {
  2839. start: this.start,
  2840. end: this.end
  2841. };
  2842. this.emit('rangechange', params);
  2843. this.emit('rangechanged', params);
  2844. }
  2845. };
  2846. /**
  2847. * Set a new start and end range. This method is the same as setRange, but
  2848. * does not trigger a range change and range changed event, and it returns
  2849. * true when the range is changed
  2850. * @param {Number} [start]
  2851. * @param {Number} [end]
  2852. * @return {Boolean} changed
  2853. * @private
  2854. */
  2855. Range.prototype._applyRange = function(start, end) {
  2856. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  2857. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  2858. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2859. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2860. diff;
  2861. // check for valid number
  2862. if (isNaN(newStart) || newStart === null) {
  2863. throw new Error('Invalid start "' + start + '"');
  2864. }
  2865. if (isNaN(newEnd) || newEnd === null) {
  2866. throw new Error('Invalid end "' + end + '"');
  2867. }
  2868. // prevent start < end
  2869. if (newEnd < newStart) {
  2870. newEnd = newStart;
  2871. }
  2872. // prevent start < min
  2873. if (min !== null) {
  2874. if (newStart < min) {
  2875. diff = (min - newStart);
  2876. newStart += diff;
  2877. newEnd += diff;
  2878. // prevent end > max
  2879. if (max != null) {
  2880. if (newEnd > max) {
  2881. newEnd = max;
  2882. }
  2883. }
  2884. }
  2885. }
  2886. // prevent end > max
  2887. if (max !== null) {
  2888. if (newEnd > max) {
  2889. diff = (newEnd - max);
  2890. newStart -= diff;
  2891. newEnd -= diff;
  2892. // prevent start < min
  2893. if (min != null) {
  2894. if (newStart < min) {
  2895. newStart = min;
  2896. }
  2897. }
  2898. }
  2899. }
  2900. // prevent (end-start) < zoomMin
  2901. if (this.options.zoomMin !== null) {
  2902. var zoomMin = parseFloat(this.options.zoomMin);
  2903. if (zoomMin < 0) {
  2904. zoomMin = 0;
  2905. }
  2906. if ((newEnd - newStart) < zoomMin) {
  2907. if ((this.end - this.start) === zoomMin) {
  2908. // ignore this action, we are already zoomed to the minimum
  2909. newStart = this.start;
  2910. newEnd = this.end;
  2911. }
  2912. else {
  2913. // zoom to the minimum
  2914. diff = (zoomMin - (newEnd - newStart));
  2915. newStart -= diff / 2;
  2916. newEnd += diff / 2;
  2917. }
  2918. }
  2919. }
  2920. // prevent (end-start) > zoomMax
  2921. if (this.options.zoomMax !== null) {
  2922. var zoomMax = parseFloat(this.options.zoomMax);
  2923. if (zoomMax < 0) {
  2924. zoomMax = 0;
  2925. }
  2926. if ((newEnd - newStart) > zoomMax) {
  2927. if ((this.end - this.start) === zoomMax) {
  2928. // ignore this action, we are already zoomed to the maximum
  2929. newStart = this.start;
  2930. newEnd = this.end;
  2931. }
  2932. else {
  2933. // zoom to the maximum
  2934. diff = ((newEnd - newStart) - zoomMax);
  2935. newStart += diff / 2;
  2936. newEnd -= diff / 2;
  2937. }
  2938. }
  2939. }
  2940. var changed = (this.start != newStart || this.end != newEnd);
  2941. this.start = newStart;
  2942. this.end = newEnd;
  2943. return changed;
  2944. };
  2945. /**
  2946. * Retrieve the current range.
  2947. * @return {Object} An object with start and end properties
  2948. */
  2949. Range.prototype.getRange = function() {
  2950. return {
  2951. start: this.start,
  2952. end: this.end
  2953. };
  2954. };
  2955. /**
  2956. * Calculate the conversion offset and scale for current range, based on
  2957. * the provided width
  2958. * @param {Number} width
  2959. * @returns {{offset: number, scale: number}} conversion
  2960. */
  2961. Range.prototype.conversion = function (width) {
  2962. return Range.conversion(this.start, this.end, width);
  2963. };
  2964. /**
  2965. * Static method to calculate the conversion offset and scale for a range,
  2966. * based on the provided start, end, and width
  2967. * @param {Number} start
  2968. * @param {Number} end
  2969. * @param {Number} width
  2970. * @returns {{offset: number, scale: number}} conversion
  2971. */
  2972. Range.conversion = function (start, end, width) {
  2973. if (width != 0 && (end - start != 0)) {
  2974. return {
  2975. offset: start,
  2976. scale: width / (end - start)
  2977. }
  2978. }
  2979. else {
  2980. return {
  2981. offset: 0,
  2982. scale: 1
  2983. };
  2984. }
  2985. };
  2986. // global (private) object to store drag params
  2987. var touchParams = {};
  2988. /**
  2989. * Start dragging horizontally or vertically
  2990. * @param {Event} event
  2991. * @param {Object} component
  2992. * @private
  2993. */
  2994. Range.prototype._onDragStart = function(event, component) {
  2995. // refuse to drag when we where pinching to prevent the timeline make a jump
  2996. // when releasing the fingers in opposite order from the touch screen
  2997. if (touchParams.ignore) return;
  2998. // TODO: reckon with option movable
  2999. touchParams.start = this.start;
  3000. touchParams.end = this.end;
  3001. var frame = component.frame;
  3002. if (frame) {
  3003. frame.style.cursor = 'move';
  3004. }
  3005. };
  3006. /**
  3007. * Perform dragging operating.
  3008. * @param {Event} event
  3009. * @param {Component} component
  3010. * @param {String} direction 'horizontal' or 'vertical'
  3011. * @private
  3012. */
  3013. Range.prototype._onDrag = function (event, component, direction) {
  3014. validateDirection(direction);
  3015. // TODO: reckon with option movable
  3016. // refuse to drag when we where pinching to prevent the timeline make a jump
  3017. // when releasing the fingers in opposite order from the touch screen
  3018. if (touchParams.ignore) return;
  3019. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3020. interval = (touchParams.end - touchParams.start),
  3021. width = (direction == 'horizontal') ? component.width : component.height,
  3022. diffRange = -delta / width * interval;
  3023. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3024. this.emit('rangechange', {
  3025. start: this.start,
  3026. end: this.end
  3027. });
  3028. };
  3029. /**
  3030. * Stop dragging operating.
  3031. * @param {event} event
  3032. * @param {Component} component
  3033. * @private
  3034. */
  3035. Range.prototype._onDragEnd = function (event, component) {
  3036. // refuse to drag when we where pinching to prevent the timeline make a jump
  3037. // when releasing the fingers in opposite order from the touch screen
  3038. if (touchParams.ignore) return;
  3039. // TODO: reckon with option movable
  3040. if (component.frame) {
  3041. component.frame.style.cursor = 'auto';
  3042. }
  3043. // fire a rangechanged event
  3044. this.emit('rangechanged', {
  3045. start: this.start,
  3046. end: this.end
  3047. });
  3048. };
  3049. /**
  3050. * Event handler for mouse wheel event, used to zoom
  3051. * Code from http://adomas.org/javascript-mouse-wheel/
  3052. * @param {Event} event
  3053. * @param {Component} component
  3054. * @param {String} direction 'horizontal' or 'vertical'
  3055. * @private
  3056. */
  3057. Range.prototype._onMouseWheel = function(event, component, direction) {
  3058. validateDirection(direction);
  3059. // TODO: reckon with option zoomable
  3060. // retrieve delta
  3061. var delta = 0;
  3062. if (event.wheelDelta) { /* IE/Opera. */
  3063. delta = event.wheelDelta / 120;
  3064. } else if (event.detail) { /* Mozilla case. */
  3065. // In Mozilla, sign of delta is different than in IE.
  3066. // Also, delta is multiple of 3.
  3067. delta = -event.detail / 3;
  3068. }
  3069. // If delta is nonzero, handle it.
  3070. // Basically, delta is now positive if wheel was scrolled up,
  3071. // and negative, if wheel was scrolled down.
  3072. if (delta) {
  3073. // perform the zoom action. Delta is normally 1 or -1
  3074. // adjust a negative delta such that zooming in with delta 0.1
  3075. // equals zooming out with a delta -0.1
  3076. var scale;
  3077. if (delta < 0) {
  3078. scale = 1 - (delta / 5);
  3079. }
  3080. else {
  3081. scale = 1 / (1 + (delta / 5)) ;
  3082. }
  3083. // calculate center, the date to zoom around
  3084. var gesture = util.fakeGesture(this, event),
  3085. pointer = getPointer(gesture.center, component.frame),
  3086. pointerDate = this._pointerToDate(component, direction, pointer);
  3087. this.zoom(scale, pointerDate);
  3088. }
  3089. // Prevent default actions caused by mouse wheel
  3090. // (else the page and timeline both zoom and scroll)
  3091. event.preventDefault();
  3092. };
  3093. /**
  3094. * Start of a touch gesture
  3095. * @private
  3096. */
  3097. Range.prototype._onTouch = function (event) {
  3098. touchParams.start = this.start;
  3099. touchParams.end = this.end;
  3100. touchParams.ignore = false;
  3101. touchParams.center = null;
  3102. // don't move the range when dragging a selected event
  3103. // TODO: it's not so neat to have to know about the state of the ItemSet
  3104. var item = ItemSet.itemFromTarget(event);
  3105. if (item && item.selected && this.options.editable) {
  3106. touchParams.ignore = true;
  3107. }
  3108. };
  3109. /**
  3110. * On start of a hold gesture
  3111. * @private
  3112. */
  3113. Range.prototype._onHold = function () {
  3114. touchParams.ignore = true;
  3115. };
  3116. /**
  3117. * Handle pinch event
  3118. * @param {Event} event
  3119. * @param {Component} component
  3120. * @param {String} direction 'horizontal' or 'vertical'
  3121. * @private
  3122. */
  3123. Range.prototype._onPinch = function (event, component, direction) {
  3124. touchParams.ignore = true;
  3125. // TODO: reckon with option zoomable
  3126. if (event.gesture.touches.length > 1) {
  3127. if (!touchParams.center) {
  3128. touchParams.center = getPointer(event.gesture.center, component.frame);
  3129. }
  3130. var scale = 1 / event.gesture.scale,
  3131. initDate = this._pointerToDate(component, direction, touchParams.center),
  3132. center = getPointer(event.gesture.center, component.frame),
  3133. date = this._pointerToDate(component, direction, center),
  3134. delta = date - initDate; // TODO: utilize delta
  3135. // calculate new start and end
  3136. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3137. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3138. // apply new range
  3139. this.setRange(newStart, newEnd);
  3140. }
  3141. };
  3142. /**
  3143. * Helper function to calculate the center date for zooming
  3144. * @param {Component} component
  3145. * @param {{x: Number, y: Number}} pointer
  3146. * @param {String} direction 'horizontal' or 'vertical'
  3147. * @return {number} date
  3148. * @private
  3149. */
  3150. Range.prototype._pointerToDate = function (component, direction, pointer) {
  3151. var conversion;
  3152. if (direction == 'horizontal') {
  3153. var width = component.width;
  3154. conversion = this.conversion(width);
  3155. return pointer.x / conversion.scale + conversion.offset;
  3156. }
  3157. else {
  3158. var height = component.height;
  3159. conversion = this.conversion(height);
  3160. return pointer.y / conversion.scale + conversion.offset;
  3161. }
  3162. };
  3163. /**
  3164. * Get the pointer location relative to the location of the dom element
  3165. * @param {{pageX: Number, pageY: Number}} touch
  3166. * @param {Element} element HTML DOM element
  3167. * @return {{x: Number, y: Number}} pointer
  3168. * @private
  3169. */
  3170. function getPointer (touch, element) {
  3171. return {
  3172. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3173. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3174. };
  3175. }
  3176. /**
  3177. * Zoom the range the given scale in or out. Start and end date will
  3178. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3179. * date around which to zoom.
  3180. * For example, try scale = 0.9 or 1.1
  3181. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3182. * values below 1 will zoom in.
  3183. * @param {Number} [center] Value representing a date around which will
  3184. * be zoomed.
  3185. */
  3186. Range.prototype.zoom = function(scale, center) {
  3187. // if centerDate is not provided, take it half between start Date and end Date
  3188. if (center == null) {
  3189. center = (this.start + this.end) / 2;
  3190. }
  3191. // calculate new start and end
  3192. var newStart = center + (this.start - center) * scale;
  3193. var newEnd = center + (this.end - center) * scale;
  3194. this.setRange(newStart, newEnd);
  3195. };
  3196. /**
  3197. * Move the range with a given delta to the left or right. Start and end
  3198. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3199. * @param {Number} delta Moving amount. Positive value will move right,
  3200. * negative value will move left
  3201. */
  3202. Range.prototype.move = function(delta) {
  3203. // zoom start Date and end Date relative to the centerDate
  3204. var diff = (this.end - this.start);
  3205. // apply new values
  3206. var newStart = this.start + diff * delta;
  3207. var newEnd = this.end + diff * delta;
  3208. // TODO: reckon with min and max range
  3209. this.start = newStart;
  3210. this.end = newEnd;
  3211. };
  3212. /**
  3213. * Move the range to a new center point
  3214. * @param {Number} moveTo New center point of the range
  3215. */
  3216. Range.prototype.moveTo = function(moveTo) {
  3217. var center = (this.start + this.end) / 2;
  3218. var diff = center - moveTo;
  3219. // calculate new start and end
  3220. var newStart = this.start - diff;
  3221. var newEnd = this.end - diff;
  3222. this.setRange(newStart, newEnd);
  3223. };
  3224. /**
  3225. * @constructor Controller
  3226. *
  3227. * A Controller controls the reflows and repaints of all components,
  3228. * and is used as an event bus for all components.
  3229. */
  3230. function Controller () {
  3231. var me = this;
  3232. this.id = util.randomUUID();
  3233. this.components = {};
  3234. /**
  3235. * Listen for a 'request-reflow' event. The controller will schedule a reflow
  3236. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3237. * is false.
  3238. */
  3239. var reflowTimer = null;
  3240. this.on('request-reflow', function requestReflow(force) {
  3241. if (force) {
  3242. me.reflow();
  3243. }
  3244. else {
  3245. if (!reflowTimer) {
  3246. reflowTimer = setTimeout(function () {
  3247. reflowTimer = null;
  3248. me.reflow();
  3249. }, 0);
  3250. }
  3251. }
  3252. });
  3253. /**
  3254. * Request a repaint. The controller will schedule a repaint
  3255. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3256. * is false.
  3257. */
  3258. var repaintTimer = null;
  3259. this.on('request-repaint', function requestRepaint(force) {
  3260. if (force) {
  3261. me.repaint();
  3262. }
  3263. else {
  3264. if (!repaintTimer) {
  3265. repaintTimer = setTimeout(function () {
  3266. repaintTimer = null;
  3267. me.repaint();
  3268. }, 0);
  3269. }
  3270. }
  3271. });
  3272. }
  3273. // Extend controller with Emitter mixin
  3274. Emitter(Controller.prototype);
  3275. /**
  3276. * Add a component to the controller
  3277. * @param {Component} component
  3278. */
  3279. Controller.prototype.add = function add(component) {
  3280. // validate the component
  3281. if (component.id == undefined) {
  3282. throw new Error('Component has no field id');
  3283. }
  3284. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3285. throw new TypeError('Component must be an instance of ' +
  3286. 'prototype Component or Controller');
  3287. }
  3288. // add the component
  3289. component.setController(this);
  3290. this.components[component.id] = component;
  3291. };
  3292. /**
  3293. * Remove a component from the controller
  3294. * @param {Component | String} component
  3295. */
  3296. Controller.prototype.remove = function remove(component) {
  3297. var id;
  3298. for (id in this.components) {
  3299. if (this.components.hasOwnProperty(id)) {
  3300. if (id == component || this.components[id] === component) {
  3301. break;
  3302. }
  3303. }
  3304. }
  3305. if (id) {
  3306. // unregister the controller (gives the component the ability to unregister
  3307. // event listeners and clean up other stuff)
  3308. this.components[id].setController(null);
  3309. delete this.components[id];
  3310. }
  3311. };
  3312. /**
  3313. * Repaint all components
  3314. */
  3315. Controller.prototype.repaint = function repaint() {
  3316. var changed = false;
  3317. // cancel any running repaint request
  3318. if (this.repaintTimer) {
  3319. clearTimeout(this.repaintTimer);
  3320. this.repaintTimer = undefined;
  3321. }
  3322. var done = {};
  3323. function repaint(component, id) {
  3324. if (!(id in done)) {
  3325. // first repaint the components on which this component is dependent
  3326. if (component.depends) {
  3327. component.depends.forEach(function (dep) {
  3328. repaint(dep, dep.id);
  3329. });
  3330. }
  3331. if (component.parent) {
  3332. repaint(component.parent, component.parent.id);
  3333. }
  3334. // repaint the component itself and mark as done
  3335. changed = component.repaint() || changed;
  3336. done[id] = true;
  3337. }
  3338. }
  3339. util.forEach(this.components, repaint);
  3340. this.emit('repaint');
  3341. // immediately reflow when needed
  3342. if (changed) {
  3343. this.reflow();
  3344. }
  3345. // TODO: limit the number of nested reflows/repaints, prevent loop
  3346. };
  3347. /**
  3348. * Reflow all components
  3349. */
  3350. Controller.prototype.reflow = function reflow() {
  3351. var resized = false;
  3352. // cancel any running repaint request
  3353. if (this.reflowTimer) {
  3354. clearTimeout(this.reflowTimer);
  3355. this.reflowTimer = undefined;
  3356. }
  3357. var done = {};
  3358. function reflow(component, id) {
  3359. if (!(id in done)) {
  3360. // first reflow the components on which this component is dependent
  3361. if (component.depends) {
  3362. component.depends.forEach(function (dep) {
  3363. reflow(dep, dep.id);
  3364. });
  3365. }
  3366. if (component.parent) {
  3367. reflow(component.parent, component.parent.id);
  3368. }
  3369. // reflow the component itself and mark as done
  3370. resized = component.reflow() || resized;
  3371. done[id] = true;
  3372. }
  3373. }
  3374. util.forEach(this.components, reflow);
  3375. this.emit('reflow');
  3376. // immediately repaint when needed
  3377. if (resized) {
  3378. this.repaint();
  3379. }
  3380. // TODO: limit the number of nested reflows/repaints, prevent loop
  3381. };
  3382. /**
  3383. * Prototype for visual components
  3384. */
  3385. function Component () {
  3386. this.id = null;
  3387. this.parent = null;
  3388. this.depends = null;
  3389. this.controller = null;
  3390. this.options = null;
  3391. this.frame = null; // main DOM element
  3392. this.top = 0;
  3393. this.left = 0;
  3394. this.width = 0;
  3395. this.height = 0;
  3396. }
  3397. /**
  3398. * Set parameters for the frame. Parameters will be merged in current parameter
  3399. * set.
  3400. * @param {Object} options Available parameters:
  3401. * {String | function} [className]
  3402. * {String | Number | function} [left]
  3403. * {String | Number | function} [top]
  3404. * {String | Number | function} [width]
  3405. * {String | Number | function} [height]
  3406. */
  3407. Component.prototype.setOptions = function setOptions(options) {
  3408. if (options) {
  3409. util.extend(this.options, options);
  3410. if (this.controller) {
  3411. this.requestRepaint();
  3412. this.requestReflow();
  3413. }
  3414. }
  3415. };
  3416. /**
  3417. * Get an option value by name
  3418. * The function will first check this.options object, and else will check
  3419. * this.defaultOptions.
  3420. * @param {String} name
  3421. * @return {*} value
  3422. */
  3423. Component.prototype.getOption = function getOption(name) {
  3424. var value;
  3425. if (this.options) {
  3426. value = this.options[name];
  3427. }
  3428. if (value === undefined && this.defaultOptions) {
  3429. value = this.defaultOptions[name];
  3430. }
  3431. return value;
  3432. };
  3433. /**
  3434. * Set controller for this component, or remove current controller by passing
  3435. * null as parameter value.
  3436. * @param {Controller | null} controller
  3437. */
  3438. Component.prototype.setController = function setController (controller) {
  3439. this.controller = controller || null;
  3440. };
  3441. /**
  3442. * Get controller of this component
  3443. * @return {Controller} controller
  3444. */
  3445. Component.prototype.getController = function getController () {
  3446. return this.controller;
  3447. };
  3448. /**
  3449. * Get the container element of the component, which can be used by a child to
  3450. * add its own widgets. Not all components do have a container for childs, in
  3451. * that case null is returned.
  3452. * @returns {HTMLElement | null} container
  3453. */
  3454. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  3455. Component.prototype.getContainer = function getContainer() {
  3456. // should be implemented by the component
  3457. return null;
  3458. };
  3459. /**
  3460. * Get the frame element of the component, the outer HTML DOM element.
  3461. * @returns {HTMLElement | null} frame
  3462. */
  3463. Component.prototype.getFrame = function getFrame() {
  3464. return this.frame;
  3465. };
  3466. /**
  3467. * Repaint the component
  3468. * @return {Boolean} changed
  3469. */
  3470. Component.prototype.repaint = function repaint() {
  3471. // should be implemented by the component
  3472. return false;
  3473. };
  3474. /**
  3475. * Reflow the component
  3476. * @return {Boolean} resized
  3477. */
  3478. Component.prototype.reflow = function reflow() {
  3479. // should be implemented by the component
  3480. return false;
  3481. };
  3482. /**
  3483. * Hide the component from the DOM
  3484. * @return {Boolean} changed
  3485. */
  3486. Component.prototype.hide = function hide() {
  3487. if (this.frame && this.frame.parentNode) {
  3488. this.frame.parentNode.removeChild(this.frame);
  3489. return true;
  3490. }
  3491. else {
  3492. return false;
  3493. }
  3494. };
  3495. /**
  3496. * Show the component in the DOM (when not already visible).
  3497. * A repaint will be executed when the component is not visible
  3498. * @return {Boolean} changed
  3499. */
  3500. Component.prototype.show = function show() {
  3501. if (!this.frame || !this.frame.parentNode) {
  3502. return this.repaint();
  3503. }
  3504. else {
  3505. return false;
  3506. }
  3507. };
  3508. /**
  3509. * Request a repaint. The controller will schedule a repaint
  3510. */
  3511. Component.prototype.requestRepaint = function requestRepaint() {
  3512. if (this.controller) {
  3513. this.controller.emit('request-repaint');
  3514. }
  3515. else {
  3516. throw new Error('Cannot request a repaint: no controller configured');
  3517. // TODO: just do a repaint when no parent is configured?
  3518. }
  3519. };
  3520. /**
  3521. * Request a reflow. The controller will schedule a reflow
  3522. */
  3523. Component.prototype.requestReflow = function requestReflow() {
  3524. if (this.controller) {
  3525. this.controller.emit('request-reflow');
  3526. }
  3527. else {
  3528. throw new Error('Cannot request a reflow: no controller configured');
  3529. // TODO: just do a reflow when no parent is configured?
  3530. }
  3531. };
  3532. /**
  3533. * A panel can contain components
  3534. * @param {Component} [parent]
  3535. * @param {Component[]} [depends] Components on which this components depends
  3536. * (except for the parent)
  3537. * @param {Object} [options] Available parameters:
  3538. * {String | Number | function} [left]
  3539. * {String | Number | function} [top]
  3540. * {String | Number | function} [width]
  3541. * {String | Number | function} [height]
  3542. * {String | function} [className]
  3543. * @constructor Panel
  3544. * @extends Component
  3545. */
  3546. function Panel(parent, depends, options) {
  3547. this.id = util.randomUUID();
  3548. this.parent = parent;
  3549. this.depends = depends;
  3550. this.options = options || {};
  3551. }
  3552. Panel.prototype = new Component();
  3553. /**
  3554. * Set options. Will extend the current options.
  3555. * @param {Object} [options] Available parameters:
  3556. * {String | function} [className]
  3557. * {String | Number | function} [left]
  3558. * {String | Number | function} [top]
  3559. * {String | Number | function} [width]
  3560. * {String | Number | function} [height]
  3561. */
  3562. Panel.prototype.setOptions = Component.prototype.setOptions;
  3563. /**
  3564. * Get the container element of the panel, which can be used by a child to
  3565. * add its own widgets.
  3566. * @returns {HTMLElement} container
  3567. */
  3568. Panel.prototype.getContainer = function () {
  3569. return this.frame;
  3570. };
  3571. /**
  3572. * Repaint the component
  3573. * @return {Boolean} changed
  3574. */
  3575. Panel.prototype.repaint = function () {
  3576. var changed = 0,
  3577. update = util.updateProperty,
  3578. asSize = util.option.asSize,
  3579. options = this.options,
  3580. frame = this.frame;
  3581. if (!frame) {
  3582. frame = document.createElement('div');
  3583. frame.className = 'vpanel';
  3584. var className = options.className;
  3585. if (className) {
  3586. if (typeof className == 'function') {
  3587. util.addClassName(frame, String(className()));
  3588. }
  3589. else {
  3590. util.addClassName(frame, String(className));
  3591. }
  3592. }
  3593. this.frame = frame;
  3594. changed += 1;
  3595. }
  3596. if (!frame.parentNode) {
  3597. if (!this.parent) {
  3598. throw new Error('Cannot repaint panel: no parent attached');
  3599. }
  3600. var parentContainer = this.parent.getContainer();
  3601. if (!parentContainer) {
  3602. throw new Error('Cannot repaint panel: parent has no container element');
  3603. }
  3604. parentContainer.appendChild(frame);
  3605. changed += 1;
  3606. }
  3607. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3608. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3609. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3610. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3611. return (changed > 0);
  3612. };
  3613. /**
  3614. * Reflow the component
  3615. * @return {Boolean} resized
  3616. */
  3617. Panel.prototype.reflow = function () {
  3618. var changed = 0,
  3619. update = util.updateProperty,
  3620. frame = this.frame;
  3621. if (frame) {
  3622. changed += update(this, 'top', frame.offsetTop);
  3623. changed += update(this, 'left', frame.offsetLeft);
  3624. changed += update(this, 'width', frame.offsetWidth);
  3625. changed += update(this, 'height', frame.offsetHeight);
  3626. }
  3627. else {
  3628. changed += 1;
  3629. }
  3630. return (changed > 0);
  3631. };
  3632. /**
  3633. * A root panel can hold components. The root panel must be initialized with
  3634. * a DOM element as container.
  3635. * @param {HTMLElement} container
  3636. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3637. * @constructor RootPanel
  3638. * @extends Panel
  3639. */
  3640. function RootPanel(container, options) {
  3641. this.id = util.randomUUID();
  3642. this.container = container;
  3643. // create functions to be used as DOM event listeners
  3644. var me = this;
  3645. this.hammer = null;
  3646. // create listeners for all interesting events, these events will be emitted
  3647. // via the controller
  3648. var events = [
  3649. 'touch', 'pinch', 'tap', 'doubletap', 'hold',
  3650. 'dragstart', 'drag', 'dragend',
  3651. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
  3652. ];
  3653. this.listeners = {};
  3654. events.forEach(function (event) {
  3655. me.listeners[event] = function () {
  3656. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  3657. me.controller.emit.apply(me.controller, args);
  3658. };
  3659. });
  3660. this.options = options || {};
  3661. this.defaultOptions = {
  3662. autoResize: true
  3663. };
  3664. }
  3665. RootPanel.prototype = new Panel();
  3666. /**
  3667. * Set options. Will extend the current options.
  3668. * @param {Object} [options] Available parameters:
  3669. * {String | function} [className]
  3670. * {String | Number | function} [left]
  3671. * {String | Number | function} [top]
  3672. * {String | Number | function} [width]
  3673. * {String | Number | function} [height]
  3674. * {Boolean | function} [autoResize]
  3675. */
  3676. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3677. /**
  3678. * Repaint the component
  3679. * @return {Boolean} changed
  3680. */
  3681. RootPanel.prototype.repaint = function () {
  3682. var changed = 0,
  3683. update = util.updateProperty,
  3684. asSize = util.option.asSize,
  3685. options = this.options,
  3686. frame = this.frame;
  3687. if (!frame) {
  3688. frame = document.createElement('div');
  3689. this.frame = frame;
  3690. this._registerListeners();
  3691. changed += 1;
  3692. }
  3693. if (!frame.parentNode) {
  3694. if (!this.container) {
  3695. throw new Error('Cannot repaint root panel: no container attached');
  3696. }
  3697. this.container.appendChild(frame);
  3698. changed += 1;
  3699. }
  3700. frame.className = 'vis timeline rootpanel ' + options.orientation +
  3701. (options.editable ? ' editable' : '');
  3702. var className = options.className;
  3703. if (className) {
  3704. util.addClassName(frame, util.option.asString(className));
  3705. }
  3706. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3707. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3708. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3709. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3710. this._updateWatch();
  3711. return (changed > 0);
  3712. };
  3713. /**
  3714. * Reflow the component
  3715. * @return {Boolean} resized
  3716. */
  3717. RootPanel.prototype.reflow = function () {
  3718. var changed = 0,
  3719. update = util.updateProperty,
  3720. frame = this.frame;
  3721. if (frame) {
  3722. changed += update(this, 'top', frame.offsetTop);
  3723. changed += update(this, 'left', frame.offsetLeft);
  3724. changed += update(this, 'width', frame.offsetWidth);
  3725. changed += update(this, 'height', frame.offsetHeight);
  3726. }
  3727. else {
  3728. changed += 1;
  3729. }
  3730. return (changed > 0);
  3731. };
  3732. /**
  3733. * Update watching for resize, depending on the current option
  3734. * @private
  3735. */
  3736. RootPanel.prototype._updateWatch = function () {
  3737. var autoResize = this.getOption('autoResize');
  3738. if (autoResize) {
  3739. this._watch();
  3740. }
  3741. else {
  3742. this._unwatch();
  3743. }
  3744. };
  3745. /**
  3746. * Watch for changes in the size of the frame. On resize, the Panel will
  3747. * automatically redraw itself.
  3748. * @private
  3749. */
  3750. RootPanel.prototype._watch = function () {
  3751. var me = this;
  3752. this._unwatch();
  3753. var checkSize = function () {
  3754. var autoResize = me.getOption('autoResize');
  3755. if (!autoResize) {
  3756. // stop watching when the option autoResize is changed to false
  3757. me._unwatch();
  3758. return;
  3759. }
  3760. if (me.frame) {
  3761. // check whether the frame is resized
  3762. if ((me.frame.clientWidth != me.width) ||
  3763. (me.frame.clientHeight != me.height)) {
  3764. me.requestReflow();
  3765. }
  3766. }
  3767. };
  3768. // TODO: automatically cleanup the event listener when the frame is deleted
  3769. util.addEventListener(window, 'resize', checkSize);
  3770. this.watchTimer = setInterval(checkSize, 1000);
  3771. };
  3772. /**
  3773. * Stop watching for a resize of the frame.
  3774. * @private
  3775. */
  3776. RootPanel.prototype._unwatch = function () {
  3777. if (this.watchTimer) {
  3778. clearInterval(this.watchTimer);
  3779. this.watchTimer = undefined;
  3780. }
  3781. // TODO: remove event listener on window.resize
  3782. };
  3783. /**
  3784. * Set controller for this component, or remove current controller by passing
  3785. * null as parameter value.
  3786. * @param {Controller | null} controller
  3787. */
  3788. RootPanel.prototype.setController = function setController (controller) {
  3789. this.controller = controller || null;
  3790. if (this.controller) {
  3791. this._registerListeners();
  3792. }
  3793. else {
  3794. this._unregisterListeners();
  3795. }
  3796. };
  3797. /**
  3798. * Register event emitters emitted by the rootpanel
  3799. * @private
  3800. */
  3801. RootPanel.prototype._registerListeners = function () {
  3802. if (this.frame && this.controller && !this.hammer) {
  3803. this.hammer = Hammer(this.frame, {
  3804. prevent_default: true
  3805. });
  3806. for (var event in this.listeners) {
  3807. if (this.listeners.hasOwnProperty(event)) {
  3808. this.hammer.on(event, this.listeners[event]);
  3809. }
  3810. }
  3811. }
  3812. };
  3813. /**
  3814. * Unregister event emitters from the rootpanel
  3815. * @private
  3816. */
  3817. RootPanel.prototype._unregisterListeners = function () {
  3818. if (this.hammer) {
  3819. for (var event in this.listeners) {
  3820. if (this.listeners.hasOwnProperty(event)) {
  3821. this.hammer.off(event, this.listeners[event]);
  3822. }
  3823. }
  3824. this.hammer = null;
  3825. }
  3826. };
  3827. /**
  3828. * A horizontal time axis
  3829. * @param {Component} parent
  3830. * @param {Component[]} [depends] Components on which this components depends
  3831. * (except for the parent)
  3832. * @param {Object} [options] See TimeAxis.setOptions for the available
  3833. * options.
  3834. * @constructor TimeAxis
  3835. * @extends Component
  3836. */
  3837. function TimeAxis (parent, depends, options) {
  3838. this.id = util.randomUUID();
  3839. this.parent = parent;
  3840. this.depends = depends;
  3841. this.dom = {
  3842. majorLines: [],
  3843. majorTexts: [],
  3844. minorLines: [],
  3845. minorTexts: [],
  3846. redundant: {
  3847. majorLines: [],
  3848. majorTexts: [],
  3849. minorLines: [],
  3850. minorTexts: []
  3851. }
  3852. };
  3853. this.props = {
  3854. range: {
  3855. start: 0,
  3856. end: 0,
  3857. minimumStep: 0
  3858. },
  3859. lineTop: 0
  3860. };
  3861. this.options = options || {};
  3862. this.defaultOptions = {
  3863. orientation: 'bottom', // supported: 'top', 'bottom'
  3864. // TODO: implement timeaxis orientations 'left' and 'right'
  3865. showMinorLabels: true,
  3866. showMajorLabels: true
  3867. };
  3868. this.conversion = null;
  3869. this.range = null;
  3870. }
  3871. TimeAxis.prototype = new Component();
  3872. // TODO: comment options
  3873. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3874. /**
  3875. * Set a range (start and end)
  3876. * @param {Range | Object} range A Range or an object containing start and end.
  3877. */
  3878. TimeAxis.prototype.setRange = function (range) {
  3879. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3880. throw new TypeError('Range must be an instance of Range, ' +
  3881. 'or an object containing start and end.');
  3882. }
  3883. this.range = range;
  3884. };
  3885. /**
  3886. * Convert a position on screen (pixels) to a datetime
  3887. * @param {int} x Position on the screen in pixels
  3888. * @return {Date} time The datetime the corresponds with given position x
  3889. */
  3890. TimeAxis.prototype.toTime = function(x) {
  3891. var conversion = this.conversion;
  3892. return new Date(x / conversion.scale + conversion.offset);
  3893. };
  3894. /**
  3895. * Convert a datetime (Date object) into a position on the screen
  3896. * @param {Date} time A date
  3897. * @return {int} x The position on the screen in pixels which corresponds
  3898. * with the given date.
  3899. * @private
  3900. */
  3901. TimeAxis.prototype.toScreen = function(time) {
  3902. var conversion = this.conversion;
  3903. return (time.valueOf() - conversion.offset) * conversion.scale;
  3904. };
  3905. /**
  3906. * Repaint the component
  3907. * @return {Boolean} changed
  3908. */
  3909. TimeAxis.prototype.repaint = function () {
  3910. var changed = 0,
  3911. update = util.updateProperty,
  3912. asSize = util.option.asSize,
  3913. options = this.options,
  3914. orientation = this.getOption('orientation'),
  3915. props = this.props,
  3916. step = this.step;
  3917. var frame = this.frame;
  3918. if (!frame) {
  3919. frame = document.createElement('div');
  3920. this.frame = frame;
  3921. changed += 1;
  3922. }
  3923. frame.className = 'axis';
  3924. // TODO: custom className?
  3925. if (!frame.parentNode) {
  3926. if (!this.parent) {
  3927. throw new Error('Cannot repaint time axis: no parent attached');
  3928. }
  3929. var parentContainer = this.parent.getContainer();
  3930. if (!parentContainer) {
  3931. throw new Error('Cannot repaint time axis: parent has no container element');
  3932. }
  3933. parentContainer.appendChild(frame);
  3934. changed += 1;
  3935. }
  3936. var parent = frame.parentNode;
  3937. if (parent) {
  3938. var beforeChild = frame.nextSibling;
  3939. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3940. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3941. (this.props.parentHeight - this.height) + 'px' :
  3942. '0px';
  3943. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3944. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3945. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3946. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3947. // get characters width and height
  3948. this._repaintMeasureChars();
  3949. if (this.step) {
  3950. this._repaintStart();
  3951. step.first();
  3952. var xFirstMajorLabel = undefined;
  3953. var max = 0;
  3954. while (step.hasNext() && max < 1000) {
  3955. max++;
  3956. var cur = step.getCurrent(),
  3957. x = this.toScreen(cur),
  3958. isMajor = step.isMajor();
  3959. // TODO: lines must have a width, such that we can create css backgrounds
  3960. if (this.getOption('showMinorLabels')) {
  3961. this._repaintMinorText(x, step.getLabelMinor());
  3962. }
  3963. if (isMajor && this.getOption('showMajorLabels')) {
  3964. if (x > 0) {
  3965. if (xFirstMajorLabel == undefined) {
  3966. xFirstMajorLabel = x;
  3967. }
  3968. this._repaintMajorText(x, step.getLabelMajor());
  3969. }
  3970. this._repaintMajorLine(x);
  3971. }
  3972. else {
  3973. this._repaintMinorLine(x);
  3974. }
  3975. step.next();
  3976. }
  3977. // create a major label on the left when needed
  3978. if (this.getOption('showMajorLabels')) {
  3979. var leftTime = this.toTime(0),
  3980. leftText = step.getLabelMajor(leftTime),
  3981. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3982. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3983. this._repaintMajorText(0, leftText);
  3984. }
  3985. }
  3986. this._repaintEnd();
  3987. }
  3988. this._repaintLine();
  3989. // put frame online again
  3990. if (beforeChild) {
  3991. parent.insertBefore(frame, beforeChild);
  3992. }
  3993. else {
  3994. parent.appendChild(frame)
  3995. }
  3996. }
  3997. return (changed > 0);
  3998. };
  3999. /**
  4000. * Start a repaint. Move all DOM elements to a redundant list, where they
  4001. * can be picked for re-use, or can be cleaned up in the end
  4002. * @private
  4003. */
  4004. TimeAxis.prototype._repaintStart = function () {
  4005. var dom = this.dom,
  4006. redundant = dom.redundant;
  4007. redundant.majorLines = dom.majorLines;
  4008. redundant.majorTexts = dom.majorTexts;
  4009. redundant.minorLines = dom.minorLines;
  4010. redundant.minorTexts = dom.minorTexts;
  4011. dom.majorLines = [];
  4012. dom.majorTexts = [];
  4013. dom.minorLines = [];
  4014. dom.minorTexts = [];
  4015. };
  4016. /**
  4017. * End a repaint. Cleanup leftover DOM elements in the redundant list
  4018. * @private
  4019. */
  4020. TimeAxis.prototype._repaintEnd = function () {
  4021. util.forEach(this.dom.redundant, function (arr) {
  4022. while (arr.length) {
  4023. var elem = arr.pop();
  4024. if (elem && elem.parentNode) {
  4025. elem.parentNode.removeChild(elem);
  4026. }
  4027. }
  4028. });
  4029. };
  4030. /**
  4031. * Create a minor label for the axis at position x
  4032. * @param {Number} x
  4033. * @param {String} text
  4034. * @private
  4035. */
  4036. TimeAxis.prototype._repaintMinorText = function (x, text) {
  4037. // reuse redundant label
  4038. var label = this.dom.redundant.minorTexts.shift();
  4039. if (!label) {
  4040. // create new label
  4041. var content = document.createTextNode('');
  4042. label = document.createElement('div');
  4043. label.appendChild(content);
  4044. label.className = 'text minor';
  4045. this.frame.appendChild(label);
  4046. }
  4047. this.dom.minorTexts.push(label);
  4048. label.childNodes[0].nodeValue = text;
  4049. label.style.left = x + 'px';
  4050. label.style.top = this.props.minorLabelTop + 'px';
  4051. //label.title = title; // TODO: this is a heavy operation
  4052. };
  4053. /**
  4054. * Create a Major label for the axis at position x
  4055. * @param {Number} x
  4056. * @param {String} text
  4057. * @private
  4058. */
  4059. TimeAxis.prototype._repaintMajorText = function (x, text) {
  4060. // reuse redundant label
  4061. var label = this.dom.redundant.majorTexts.shift();
  4062. if (!label) {
  4063. // create label
  4064. var content = document.createTextNode(text);
  4065. label = document.createElement('div');
  4066. label.className = 'text major';
  4067. label.appendChild(content);
  4068. this.frame.appendChild(label);
  4069. }
  4070. this.dom.majorTexts.push(label);
  4071. label.childNodes[0].nodeValue = text;
  4072. label.style.top = this.props.majorLabelTop + 'px';
  4073. label.style.left = x + 'px';
  4074. //label.title = title; // TODO: this is a heavy operation
  4075. };
  4076. /**
  4077. * Create a minor line for the axis at position x
  4078. * @param {Number} x
  4079. * @private
  4080. */
  4081. TimeAxis.prototype._repaintMinorLine = function (x) {
  4082. // reuse redundant line
  4083. var line = this.dom.redundant.minorLines.shift();
  4084. if (!line) {
  4085. // create vertical line
  4086. line = document.createElement('div');
  4087. line.className = 'grid vertical minor';
  4088. this.frame.appendChild(line);
  4089. }
  4090. this.dom.minorLines.push(line);
  4091. var props = this.props;
  4092. line.style.top = props.minorLineTop + 'px';
  4093. line.style.height = props.minorLineHeight + 'px';
  4094. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4095. };
  4096. /**
  4097. * Create a Major line for the axis at position x
  4098. * @param {Number} x
  4099. * @private
  4100. */
  4101. TimeAxis.prototype._repaintMajorLine = function (x) {
  4102. // reuse redundant line
  4103. var line = this.dom.redundant.majorLines.shift();
  4104. if (!line) {
  4105. // create vertical line
  4106. line = document.createElement('DIV');
  4107. line.className = 'grid vertical major';
  4108. this.frame.appendChild(line);
  4109. }
  4110. this.dom.majorLines.push(line);
  4111. var props = this.props;
  4112. line.style.top = props.majorLineTop + 'px';
  4113. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4114. line.style.height = props.majorLineHeight + 'px';
  4115. };
  4116. /**
  4117. * Repaint the horizontal line for the axis
  4118. * @private
  4119. */
  4120. TimeAxis.prototype._repaintLine = function() {
  4121. var line = this.dom.line,
  4122. frame = this.frame,
  4123. options = this.options;
  4124. // line before all axis elements
  4125. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4126. if (line) {
  4127. // put this line at the end of all childs
  4128. frame.removeChild(line);
  4129. frame.appendChild(line);
  4130. }
  4131. else {
  4132. // create the axis line
  4133. line = document.createElement('div');
  4134. line.className = 'grid horizontal major';
  4135. frame.appendChild(line);
  4136. this.dom.line = line;
  4137. }
  4138. line.style.top = this.props.lineTop + 'px';
  4139. }
  4140. else {
  4141. if (line && line.parentElement) {
  4142. frame.removeChild(line.line);
  4143. delete this.dom.line;
  4144. }
  4145. }
  4146. };
  4147. /**
  4148. * Create characters used to determine the size of text on the axis
  4149. * @private
  4150. */
  4151. TimeAxis.prototype._repaintMeasureChars = function () {
  4152. // calculate the width and height of a single character
  4153. // this is used to calculate the step size, and also the positioning of the
  4154. // axis
  4155. var dom = this.dom,
  4156. text;
  4157. if (!dom.measureCharMinor) {
  4158. text = document.createTextNode('0');
  4159. var measureCharMinor = document.createElement('DIV');
  4160. measureCharMinor.className = 'text minor measure';
  4161. measureCharMinor.appendChild(text);
  4162. this.frame.appendChild(measureCharMinor);
  4163. dom.measureCharMinor = measureCharMinor;
  4164. }
  4165. if (!dom.measureCharMajor) {
  4166. text = document.createTextNode('0');
  4167. var measureCharMajor = document.createElement('DIV');
  4168. measureCharMajor.className = 'text major measure';
  4169. measureCharMajor.appendChild(text);
  4170. this.frame.appendChild(measureCharMajor);
  4171. dom.measureCharMajor = measureCharMajor;
  4172. }
  4173. };
  4174. /**
  4175. * Reflow the component
  4176. * @return {Boolean} resized
  4177. */
  4178. TimeAxis.prototype.reflow = function () {
  4179. var changed = 0,
  4180. update = util.updateProperty,
  4181. frame = this.frame,
  4182. range = this.range;
  4183. if (!range) {
  4184. throw new Error('Cannot repaint time axis: no range configured');
  4185. }
  4186. if (frame) {
  4187. changed += update(this, 'top', frame.offsetTop);
  4188. changed += update(this, 'left', frame.offsetLeft);
  4189. // calculate size of a character
  4190. var props = this.props,
  4191. showMinorLabels = this.getOption('showMinorLabels'),
  4192. showMajorLabels = this.getOption('showMajorLabels'),
  4193. measureCharMinor = this.dom.measureCharMinor,
  4194. measureCharMajor = this.dom.measureCharMajor;
  4195. if (measureCharMinor) {
  4196. props.minorCharHeight = measureCharMinor.clientHeight;
  4197. props.minorCharWidth = measureCharMinor.clientWidth;
  4198. }
  4199. if (measureCharMajor) {
  4200. props.majorCharHeight = measureCharMajor.clientHeight;
  4201. props.majorCharWidth = measureCharMajor.clientWidth;
  4202. }
  4203. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4204. if (parentHeight != props.parentHeight) {
  4205. props.parentHeight = parentHeight;
  4206. changed += 1;
  4207. }
  4208. switch (this.getOption('orientation')) {
  4209. case 'bottom':
  4210. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4211. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4212. props.minorLabelTop = 0;
  4213. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4214. props.minorLineTop = -this.top;
  4215. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4216. props.minorLineWidth = 1; // TODO: really calculate width
  4217. props.majorLineTop = -this.top;
  4218. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4219. props.majorLineWidth = 1; // TODO: really calculate width
  4220. props.lineTop = 0;
  4221. break;
  4222. case 'top':
  4223. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4224. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4225. props.majorLabelTop = 0;
  4226. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4227. props.minorLineTop = props.minorLabelTop;
  4228. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4229. props.minorLineWidth = 1; // TODO: really calculate width
  4230. props.majorLineTop = 0;
  4231. props.majorLineHeight = Math.max(parentHeight - this.top);
  4232. props.majorLineWidth = 1; // TODO: really calculate width
  4233. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4234. break;
  4235. default:
  4236. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4237. }
  4238. var height = props.minorLabelHeight + props.majorLabelHeight;
  4239. changed += update(this, 'width', frame.offsetWidth);
  4240. changed += update(this, 'height', height);
  4241. // calculate range and step
  4242. this._updateConversion();
  4243. var start = util.convert(range.start, 'Number'),
  4244. end = util.convert(range.end, 'Number'),
  4245. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  4246. -this.toTime(0).valueOf();
  4247. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  4248. changed += update(props.range, 'start', start);
  4249. changed += update(props.range, 'end', end);
  4250. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4251. }
  4252. return (changed > 0);
  4253. };
  4254. /**
  4255. * Calculate the scale and offset to convert a position on screen to the
  4256. * corresponding date and vice versa.
  4257. * After the method _updateConversion is executed once, the methods toTime
  4258. * and toScreen can be used.
  4259. * @private
  4260. */
  4261. TimeAxis.prototype._updateConversion = function() {
  4262. var range = this.range;
  4263. if (!range) {
  4264. throw new Error('No range configured');
  4265. }
  4266. if (range.conversion) {
  4267. this.conversion = range.conversion(this.width);
  4268. }
  4269. else {
  4270. this.conversion = Range.conversion(range.start, range.end, this.width);
  4271. }
  4272. };
  4273. /**
  4274. * Snap a date to a rounded value.
  4275. * The snap intervals are dependent on the current scale and step.
  4276. * @param {Date} date the date to be snapped.
  4277. * @return {Date} snappedDate
  4278. */
  4279. TimeAxis.prototype.snap = function snap (date) {
  4280. return this.step.snap(date);
  4281. };
  4282. /**
  4283. * A current time bar
  4284. * @param {Component} parent
  4285. * @param {Component[]} [depends] Components on which this components depends
  4286. * (except for the parent)
  4287. * @param {Object} [options] Available parameters:
  4288. * {Boolean} [showCurrentTime]
  4289. * @constructor CurrentTime
  4290. * @extends Component
  4291. */
  4292. function CurrentTime (parent, depends, options) {
  4293. this.id = util.randomUUID();
  4294. this.parent = parent;
  4295. this.depends = depends;
  4296. this.options = options || {};
  4297. this.defaultOptions = {
  4298. showCurrentTime: false
  4299. };
  4300. }
  4301. CurrentTime.prototype = new Component();
  4302. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  4303. /**
  4304. * Get the container element of the bar, which can be used by a child to
  4305. * add its own widgets.
  4306. * @returns {HTMLElement} container
  4307. */
  4308. CurrentTime.prototype.getContainer = function () {
  4309. return this.frame;
  4310. };
  4311. /**
  4312. * Repaint the component
  4313. * @return {Boolean} changed
  4314. */
  4315. CurrentTime.prototype.repaint = function () {
  4316. var bar = this.frame,
  4317. parent = this.parent,
  4318. parentContainer = parent.parent.getContainer();
  4319. if (!parent) {
  4320. throw new Error('Cannot repaint bar: no parent attached');
  4321. }
  4322. if (!parentContainer) {
  4323. throw new Error('Cannot repaint bar: parent has no container element');
  4324. }
  4325. if (!this.getOption('showCurrentTime')) {
  4326. if (bar) {
  4327. parentContainer.removeChild(bar);
  4328. delete this.frame;
  4329. }
  4330. return false;
  4331. }
  4332. if (!bar) {
  4333. bar = document.createElement('div');
  4334. bar.className = 'currenttime';
  4335. bar.style.position = 'absolute';
  4336. bar.style.top = '0px';
  4337. bar.style.height = '100%';
  4338. parentContainer.appendChild(bar);
  4339. this.frame = bar;
  4340. }
  4341. if (!parent.conversion) {
  4342. parent._updateConversion();
  4343. }
  4344. var now = new Date();
  4345. var x = parent.toScreen(now);
  4346. bar.style.left = x + 'px';
  4347. bar.title = 'Current time: ' + now;
  4348. // start a timer to adjust for the new time
  4349. if (this.currentTimeTimer !== undefined) {
  4350. clearTimeout(this.currentTimeTimer);
  4351. delete this.currentTimeTimer;
  4352. }
  4353. var timeline = this;
  4354. var interval = 1 / parent.conversion.scale / 2;
  4355. if (interval < 30) {
  4356. interval = 30;
  4357. }
  4358. this.currentTimeTimer = setTimeout(function() {
  4359. timeline.repaint();
  4360. }, interval);
  4361. return false;
  4362. };
  4363. /**
  4364. * A custom time bar
  4365. * @param {Component} parent
  4366. * @param {Component[]} [depends] Components on which this components depends
  4367. * (except for the parent)
  4368. * @param {Object} [options] Available parameters:
  4369. * {Boolean} [showCustomTime]
  4370. * @constructor CustomTime
  4371. * @extends Component
  4372. */
  4373. function CustomTime (parent, depends, options) {
  4374. this.id = util.randomUUID();
  4375. this.parent = parent;
  4376. this.depends = depends;
  4377. this.options = options || {};
  4378. this.defaultOptions = {
  4379. showCustomTime: false
  4380. };
  4381. this.customTime = new Date();
  4382. this.eventParams = {}; // stores state parameters while dragging the bar
  4383. }
  4384. CustomTime.prototype = new Component();
  4385. Emitter(CustomTime.prototype);
  4386. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4387. /**
  4388. * Get the container element of the bar, which can be used by a child to
  4389. * add its own widgets.
  4390. * @returns {HTMLElement} container
  4391. */
  4392. CustomTime.prototype.getContainer = function () {
  4393. return this.frame;
  4394. };
  4395. /**
  4396. * Repaint the component
  4397. * @return {Boolean} changed
  4398. */
  4399. CustomTime.prototype.repaint = function () {
  4400. var bar = this.frame,
  4401. parent = this.parent;
  4402. if (!parent) {
  4403. throw new Error('Cannot repaint bar: no parent attached');
  4404. }
  4405. var parentContainer = parent.parent.getContainer();
  4406. if (!parentContainer) {
  4407. throw new Error('Cannot repaint bar: parent has no container element');
  4408. }
  4409. if (!this.getOption('showCustomTime')) {
  4410. if (bar) {
  4411. parentContainer.removeChild(bar);
  4412. delete this.frame;
  4413. }
  4414. return false;
  4415. }
  4416. if (!bar) {
  4417. bar = document.createElement('div');
  4418. bar.className = 'customtime';
  4419. bar.style.position = 'absolute';
  4420. bar.style.top = '0px';
  4421. bar.style.height = '100%';
  4422. parentContainer.appendChild(bar);
  4423. var drag = document.createElement('div');
  4424. drag.style.position = 'relative';
  4425. drag.style.top = '0px';
  4426. drag.style.left = '-10px';
  4427. drag.style.height = '100%';
  4428. drag.style.width = '20px';
  4429. bar.appendChild(drag);
  4430. this.frame = bar;
  4431. // attach event listeners
  4432. this.hammer = Hammer(bar, {
  4433. prevent_default: true
  4434. });
  4435. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4436. this.hammer.on('drag', this._onDrag.bind(this));
  4437. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4438. }
  4439. if (!parent.conversion) {
  4440. parent._updateConversion();
  4441. }
  4442. var x = parent.toScreen(this.customTime);
  4443. bar.style.left = x + 'px';
  4444. bar.title = 'Time: ' + this.customTime;
  4445. return false;
  4446. };
  4447. /**
  4448. * Set custom time.
  4449. * @param {Date} time
  4450. */
  4451. CustomTime.prototype.setCustomTime = function(time) {
  4452. this.customTime = new Date(time.valueOf());
  4453. this.repaint();
  4454. };
  4455. /**
  4456. * Retrieve the current custom time.
  4457. * @return {Date} customTime
  4458. */
  4459. CustomTime.prototype.getCustomTime = function() {
  4460. return new Date(this.customTime.valueOf());
  4461. };
  4462. /**
  4463. * Start moving horizontally
  4464. * @param {Event} event
  4465. * @private
  4466. */
  4467. CustomTime.prototype._onDragStart = function(event) {
  4468. this.eventParams.customTime = this.customTime;
  4469. event.stopPropagation();
  4470. event.preventDefault();
  4471. };
  4472. /**
  4473. * Perform moving operating.
  4474. * @param {Event} event
  4475. * @private
  4476. */
  4477. CustomTime.prototype._onDrag = function (event) {
  4478. var deltaX = event.gesture.deltaX,
  4479. x = this.parent.toScreen(this.eventParams.customTime) + deltaX,
  4480. time = this.parent.toTime(x);
  4481. this.setCustomTime(time);
  4482. // fire a timechange event
  4483. if (this.controller) {
  4484. this.controller.emit('timechange', {
  4485. time: this.customTime
  4486. })
  4487. }
  4488. event.stopPropagation();
  4489. event.preventDefault();
  4490. };
  4491. /**
  4492. * Stop moving operating.
  4493. * @param {event} event
  4494. * @private
  4495. */
  4496. CustomTime.prototype._onDragEnd = function (event) {
  4497. // fire a timechanged event
  4498. if (this.controller) {
  4499. this.controller.emit('timechanged', {
  4500. time: this.customTime
  4501. })
  4502. }
  4503. event.stopPropagation();
  4504. event.preventDefault();
  4505. };
  4506. /**
  4507. * An ItemSet holds a set of items and ranges which can be displayed in a
  4508. * range. The width is determined by the parent of the ItemSet, and the height
  4509. * is determined by the size of the items.
  4510. * @param {Component} parent
  4511. * @param {Component[]} [depends] Components on which this components depends
  4512. * (except for the parent)
  4513. * @param {Object} [options] See ItemSet.setOptions for the available
  4514. * options.
  4515. * @constructor ItemSet
  4516. * @extends Panel
  4517. */
  4518. // TODO: improve performance by replacing all Array.forEach with a for loop
  4519. function ItemSet(parent, depends, options) {
  4520. this.id = util.randomUUID();
  4521. this.parent = parent;
  4522. this.depends = depends;
  4523. // event listeners
  4524. this.eventListeners = {
  4525. dragstart: this._onDragStart.bind(this),
  4526. drag: this._onDrag.bind(this),
  4527. dragend: this._onDragEnd.bind(this)
  4528. };
  4529. // one options object is shared by this itemset and all its items
  4530. this.options = options || {};
  4531. this.defaultOptions = {
  4532. type: 'box',
  4533. align: 'center',
  4534. orientation: 'bottom',
  4535. margin: {
  4536. axis: 20,
  4537. item: 10
  4538. },
  4539. padding: 5
  4540. };
  4541. this.dom = {};
  4542. var me = this;
  4543. this.itemsData = null; // DataSet
  4544. this.range = null; // Range or Object {start: number, end: number}
  4545. // data change listeners
  4546. this.listeners = {
  4547. 'add': function (event, params, senderId) {
  4548. if (senderId != me.id) {
  4549. me._onAdd(params.items);
  4550. }
  4551. },
  4552. 'update': function (event, params, senderId) {
  4553. if (senderId != me.id) {
  4554. me._onUpdate(params.items);
  4555. }
  4556. },
  4557. 'remove': function (event, params, senderId) {
  4558. if (senderId != me.id) {
  4559. me._onRemove(params.items);
  4560. }
  4561. }
  4562. };
  4563. this.items = {}; // object with an Item for every data item
  4564. this.selection = []; // list with the ids of all selected nodes
  4565. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4566. this.stack = new Stack(this, Object.create(this.options));
  4567. this.conversion = null;
  4568. this.touchParams = {}; // stores properties while dragging
  4569. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4570. }
  4571. ItemSet.prototype = new Panel();
  4572. // available item types will be registered here
  4573. ItemSet.types = {
  4574. box: ItemBox,
  4575. range: ItemRange,
  4576. rangeoverflow: ItemRangeOverflow,
  4577. point: ItemPoint
  4578. };
  4579. /**
  4580. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4581. * @param {Object} [options] The following options are available:
  4582. * {String | function} [className]
  4583. * class name for the itemset
  4584. * {String} [type]
  4585. * Default type for the items. Choose from 'box'
  4586. * (default), 'point', or 'range'. The default
  4587. * Style can be overwritten by individual items.
  4588. * {String} align
  4589. * Alignment for the items, only applicable for
  4590. * ItemBox. Choose 'center' (default), 'left', or
  4591. * 'right'.
  4592. * {String} orientation
  4593. * Orientation of the item set. Choose 'top' or
  4594. * 'bottom' (default).
  4595. * {Number} margin.axis
  4596. * Margin between the axis and the items in pixels.
  4597. * Default is 20.
  4598. * {Number} margin.item
  4599. * Margin between items in pixels. Default is 10.
  4600. * {Number} padding
  4601. * Padding of the contents of an item in pixels.
  4602. * Must correspond with the items css. Default is 5.
  4603. * {Function} snap
  4604. * Function to let items snap to nice dates when
  4605. * dragging items.
  4606. */
  4607. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4608. /**
  4609. * Set controller for this component
  4610. * @param {Controller | null} controller
  4611. */
  4612. ItemSet.prototype.setController = function setController (controller) {
  4613. var event;
  4614. // unregister old event listeners
  4615. if (this.controller) {
  4616. for (event in this.eventListeners) {
  4617. if (this.eventListeners.hasOwnProperty(event)) {
  4618. this.controller.off(event, this.eventListeners[event]);
  4619. }
  4620. }
  4621. }
  4622. this.controller = controller || null;
  4623. // register new event listeners
  4624. if (this.controller) {
  4625. for (event in this.eventListeners) {
  4626. if (this.eventListeners.hasOwnProperty(event)) {
  4627. this.controller.on(event, this.eventListeners[event]);
  4628. }
  4629. }
  4630. }
  4631. };
  4632. // attach event listeners for dragging items to the controller
  4633. (function (me) {
  4634. var _controller = null;
  4635. var _onDragStart = null;
  4636. var _onDrag = null;
  4637. var _onDragEnd = null;
  4638. Object.defineProperty(me, 'controller', {
  4639. get: function () {
  4640. return _controller;
  4641. },
  4642. set: function (controller) {
  4643. }
  4644. });
  4645. }) (this);
  4646. /**
  4647. * Set range (start and end).
  4648. * @param {Range | Object} range A Range or an object containing start and end.
  4649. */
  4650. ItemSet.prototype.setRange = function setRange(range) {
  4651. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4652. throw new TypeError('Range must be an instance of Range, ' +
  4653. 'or an object containing start and end.');
  4654. }
  4655. this.range = range;
  4656. };
  4657. /**
  4658. * Set selected items by their id. Replaces the current selection
  4659. * Unknown id's are silently ignored.
  4660. * @param {Array} [ids] An array with zero or more id's of the items to be
  4661. * selected. If ids is an empty array, all items will be
  4662. * unselected.
  4663. */
  4664. ItemSet.prototype.setSelection = function setSelection(ids) {
  4665. var i, ii, id, item, selection;
  4666. if (ids) {
  4667. if (!Array.isArray(ids)) {
  4668. throw new TypeError('Array expected');
  4669. }
  4670. // unselect currently selected items
  4671. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4672. id = this.selection[i];
  4673. item = this.items[id];
  4674. if (item) item.unselect();
  4675. }
  4676. // select items
  4677. this.selection = [];
  4678. for (i = 0, ii = ids.length; i < ii; i++) {
  4679. id = ids[i];
  4680. item = this.items[id];
  4681. if (item) {
  4682. this.selection.push(id);
  4683. item.select();
  4684. }
  4685. }
  4686. if (this.controller) {
  4687. this.requestRepaint();
  4688. }
  4689. }
  4690. };
  4691. /**
  4692. * Get the selected items by their id
  4693. * @return {Array} ids The ids of the selected items
  4694. */
  4695. ItemSet.prototype.getSelection = function getSelection() {
  4696. return this.selection.concat([]);
  4697. };
  4698. /**
  4699. * Deselect a selected item
  4700. * @param {String | Number} id
  4701. * @private
  4702. */
  4703. ItemSet.prototype._deselect = function _deselect(id) {
  4704. var selection = this.selection;
  4705. for (var i = 0, ii = selection.length; i < ii; i++) {
  4706. if (selection[i] == id) { // non-strict comparison!
  4707. selection.splice(i, 1);
  4708. break;
  4709. }
  4710. }
  4711. };
  4712. /**
  4713. * Repaint the component
  4714. * @return {Boolean} changed
  4715. */
  4716. ItemSet.prototype.repaint = function repaint() {
  4717. var changed = 0,
  4718. update = util.updateProperty,
  4719. asSize = util.option.asSize,
  4720. options = this.options,
  4721. orientation = this.getOption('orientation'),
  4722. defaultOptions = this.defaultOptions,
  4723. frame = this.frame;
  4724. if (!frame) {
  4725. frame = document.createElement('div');
  4726. frame.className = 'itemset';
  4727. frame['timeline-itemset'] = this;
  4728. var className = options.className;
  4729. if (className) {
  4730. util.addClassName(frame, util.option.asString(className));
  4731. }
  4732. // create background panel
  4733. var background = document.createElement('div');
  4734. background.className = 'background';
  4735. frame.appendChild(background);
  4736. this.dom.background = background;
  4737. // create foreground panel
  4738. var foreground = document.createElement('div');
  4739. foreground.className = 'foreground';
  4740. frame.appendChild(foreground);
  4741. this.dom.foreground = foreground;
  4742. // create axis panel
  4743. var axis = document.createElement('div');
  4744. axis.className = 'itemset-axis';
  4745. //frame.appendChild(axis);
  4746. this.dom.axis = axis;
  4747. this.frame = frame;
  4748. changed += 1;
  4749. }
  4750. if (!this.parent) {
  4751. throw new Error('Cannot repaint itemset: no parent attached');
  4752. }
  4753. var parentContainer = this.parent.getContainer();
  4754. if (!parentContainer) {
  4755. throw new Error('Cannot repaint itemset: parent has no container element');
  4756. }
  4757. if (!frame.parentNode) {
  4758. parentContainer.appendChild(frame);
  4759. changed += 1;
  4760. }
  4761. if (!this.dom.axis.parentNode) {
  4762. parentContainer.appendChild(this.dom.axis);
  4763. changed += 1;
  4764. }
  4765. // reposition frame
  4766. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4767. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4768. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4769. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4770. // reposition axis
  4771. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4772. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4773. if (orientation == 'bottom') {
  4774. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4775. }
  4776. else { // orientation == 'top'
  4777. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4778. }
  4779. this._updateConversion();
  4780. var me = this,
  4781. queue = this.queue,
  4782. itemsData = this.itemsData,
  4783. items = this.items,
  4784. dataOptions = {
  4785. // TODO: cleanup
  4786. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  4787. };
  4788. // show/hide added/changed/removed items
  4789. for (var id in queue) {
  4790. if (queue.hasOwnProperty(id)) {
  4791. var entry = queue[id],
  4792. item = items[id],
  4793. action = entry.action;
  4794. //noinspection FallthroughInSwitchStatementJS
  4795. switch (action) {
  4796. case 'add':
  4797. case 'update':
  4798. var itemData = itemsData && itemsData.get(id, dataOptions);
  4799. if (itemData) {
  4800. var type = itemData.type ||
  4801. (itemData.start && itemData.end && 'range') ||
  4802. options.type ||
  4803. 'box';
  4804. var constructor = ItemSet.types[type];
  4805. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4806. if (item) {
  4807. // update item
  4808. if (!constructor || !(item instanceof constructor)) {
  4809. // item type has changed, hide and delete the item
  4810. changed += item.hide();
  4811. item = null;
  4812. }
  4813. else {
  4814. item.data = itemData; // TODO: create a method item.setData ?
  4815. changed++;
  4816. }
  4817. }
  4818. if (!item) {
  4819. // create item
  4820. if (constructor) {
  4821. item = new constructor(me, itemData, options, defaultOptions);
  4822. item.id = entry.id; // we take entry.id, as id itself is stringified
  4823. changed++;
  4824. }
  4825. else {
  4826. throw new TypeError('Unknown item type "' + type + '"');
  4827. }
  4828. }
  4829. // force a repaint (not only a reposition)
  4830. item.repaint();
  4831. items[id] = item;
  4832. }
  4833. // update queue
  4834. delete queue[id];
  4835. break;
  4836. case 'remove':
  4837. if (item) {
  4838. // remove the item from the set selected items
  4839. if (item.selected) {
  4840. me._deselect(id);
  4841. }
  4842. // remove DOM of the item
  4843. changed += item.hide();
  4844. }
  4845. // update lists
  4846. delete items[id];
  4847. delete queue[id];
  4848. break;
  4849. default:
  4850. console.log('Error: unknown action "' + action + '"');
  4851. }
  4852. }
  4853. }
  4854. // reposition all items. Show items only when in the visible area
  4855. util.forEach(this.items, function (item) {
  4856. if (item.visible) {
  4857. changed += item.show();
  4858. item.reposition();
  4859. }
  4860. else {
  4861. changed += item.hide();
  4862. }
  4863. });
  4864. return (changed > 0);
  4865. };
  4866. /**
  4867. * Get the foreground container element
  4868. * @return {HTMLElement} foreground
  4869. */
  4870. ItemSet.prototype.getForeground = function getForeground() {
  4871. return this.dom.foreground;
  4872. };
  4873. /**
  4874. * Get the background container element
  4875. * @return {HTMLElement} background
  4876. */
  4877. ItemSet.prototype.getBackground = function getBackground() {
  4878. return this.dom.background;
  4879. };
  4880. /**
  4881. * Get the axis container element
  4882. * @return {HTMLElement} axis
  4883. */
  4884. ItemSet.prototype.getAxis = function getAxis() {
  4885. return this.dom.axis;
  4886. };
  4887. /**
  4888. * Reflow the component
  4889. * @return {Boolean} resized
  4890. */
  4891. ItemSet.prototype.reflow = function reflow () {
  4892. var changed = 0,
  4893. options = this.options,
  4894. marginAxis = (options.margin && 'axis' in options.margin) ? options.margin.axis : this.defaultOptions.margin.axis,
  4895. marginItem = (options.margin && 'item' in options.margin) ? options.margin.item : this.defaultOptions.margin.item,
  4896. update = util.updateProperty,
  4897. asNumber = util.option.asNumber,
  4898. asSize = util.option.asSize,
  4899. frame = this.frame;
  4900. if (frame) {
  4901. this._updateConversion();
  4902. util.forEach(this.items, function (item) {
  4903. changed += item.reflow();
  4904. });
  4905. // TODO: stack.update should be triggered via an event, in stack itself
  4906. // TODO: only update the stack when there are changed items
  4907. this.stack.update();
  4908. var maxHeight = asNumber(options.maxHeight);
  4909. var fixedHeight = (asSize(options.height) != null);
  4910. var height;
  4911. if (fixedHeight) {
  4912. height = frame.offsetHeight;
  4913. }
  4914. else {
  4915. // height is not specified, determine the height from the height and positioned items
  4916. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4917. if (visibleItems.length) {
  4918. var min = visibleItems[0].top;
  4919. var max = visibleItems[0].top + visibleItems[0].height;
  4920. util.forEach(visibleItems, function (item) {
  4921. min = Math.min(min, item.top);
  4922. max = Math.max(max, (item.top + item.height));
  4923. });
  4924. height = (max - min) + marginAxis + marginItem;
  4925. }
  4926. else {
  4927. height = marginAxis + marginItem;
  4928. }
  4929. }
  4930. if (maxHeight != null) {
  4931. height = Math.min(height, maxHeight);
  4932. }
  4933. changed += update(this, 'height', height);
  4934. // calculate height from items
  4935. changed += update(this, 'top', frame.offsetTop);
  4936. changed += update(this, 'left', frame.offsetLeft);
  4937. changed += update(this, 'width', frame.offsetWidth);
  4938. }
  4939. else {
  4940. changed += 1;
  4941. }
  4942. return (changed > 0);
  4943. };
  4944. /**
  4945. * Hide this component from the DOM
  4946. * @return {Boolean} changed
  4947. */
  4948. ItemSet.prototype.hide = function hide() {
  4949. var changed = false;
  4950. // remove the DOM
  4951. if (this.frame && this.frame.parentNode) {
  4952. this.frame.parentNode.removeChild(this.frame);
  4953. changed = true;
  4954. }
  4955. if (this.dom.axis && this.dom.axis.parentNode) {
  4956. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4957. changed = true;
  4958. }
  4959. return changed;
  4960. };
  4961. /**
  4962. * Set items
  4963. * @param {vis.DataSet | null} items
  4964. */
  4965. ItemSet.prototype.setItems = function setItems(items) {
  4966. var me = this,
  4967. ids,
  4968. oldItemsData = this.itemsData;
  4969. // replace the dataset
  4970. if (!items) {
  4971. this.itemsData = null;
  4972. }
  4973. else if (items instanceof DataSet || items instanceof DataView) {
  4974. this.itemsData = items;
  4975. }
  4976. else {
  4977. throw new TypeError('Data must be an instance of DataSet');
  4978. }
  4979. if (oldItemsData) {
  4980. // unsubscribe from old dataset
  4981. util.forEach(this.listeners, function (callback, event) {
  4982. oldItemsData.unsubscribe(event, callback);
  4983. });
  4984. // remove all drawn items
  4985. ids = oldItemsData.getIds();
  4986. this._onRemove(ids);
  4987. }
  4988. if (this.itemsData) {
  4989. // subscribe to new dataset
  4990. var id = this.id;
  4991. util.forEach(this.listeners, function (callback, event) {
  4992. me.itemsData.on(event, callback, id);
  4993. });
  4994. // draw all new items
  4995. ids = this.itemsData.getIds();
  4996. this._onAdd(ids);
  4997. }
  4998. };
  4999. /**
  5000. * Get the current items items
  5001. * @returns {vis.DataSet | null}
  5002. */
  5003. ItemSet.prototype.getItems = function getItems() {
  5004. return this.itemsData;
  5005. };
  5006. /**
  5007. * Remove an item by its id
  5008. * @param {String | Number} id
  5009. */
  5010. ItemSet.prototype.removeItem = function removeItem (id) {
  5011. var item = this.itemsData.get(id),
  5012. dataset = this._myDataSet();
  5013. if (item) {
  5014. // confirm deletion
  5015. this.options.onRemove(item, function (item) {
  5016. if (item) {
  5017. dataset.remove(item);
  5018. }
  5019. });
  5020. }
  5021. };
  5022. /**
  5023. * Handle updated items
  5024. * @param {Number[]} ids
  5025. * @private
  5026. */
  5027. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  5028. this._toQueue('update', ids);
  5029. };
  5030. /**
  5031. * Handle changed items
  5032. * @param {Number[]} ids
  5033. * @private
  5034. */
  5035. ItemSet.prototype._onAdd = function _onAdd(ids) {
  5036. this._toQueue('add', ids);
  5037. };
  5038. /**
  5039. * Handle removed items
  5040. * @param {Number[]} ids
  5041. * @private
  5042. */
  5043. ItemSet.prototype._onRemove = function _onRemove(ids) {
  5044. this._toQueue('remove', ids);
  5045. };
  5046. /**
  5047. * Put items in the queue to be added/updated/remove
  5048. * @param {String} action can be 'add', 'update', 'remove'
  5049. * @param {Number[]} ids
  5050. */
  5051. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  5052. var queue = this.queue;
  5053. ids.forEach(function (id) {
  5054. queue[id] = {
  5055. id: id,
  5056. action: action
  5057. };
  5058. });
  5059. if (this.controller) {
  5060. //this.requestReflow();
  5061. this.requestRepaint();
  5062. }
  5063. };
  5064. /**
  5065. * Calculate the scale and offset to convert a position on screen to the
  5066. * corresponding date and vice versa.
  5067. * After the method _updateConversion is executed once, the methods toTime
  5068. * and toScreen can be used.
  5069. * @private
  5070. */
  5071. ItemSet.prototype._updateConversion = function _updateConversion() {
  5072. var range = this.range;
  5073. if (!range) {
  5074. throw new Error('No range configured');
  5075. }
  5076. if (range.conversion) {
  5077. this.conversion = range.conversion(this.width);
  5078. }
  5079. else {
  5080. this.conversion = Range.conversion(range.start, range.end, this.width);
  5081. }
  5082. };
  5083. /**
  5084. * Convert a position on screen (pixels) to a datetime
  5085. * Before this method can be used, the method _updateConversion must be
  5086. * executed once.
  5087. * @param {int} x Position on the screen in pixels
  5088. * @return {Date} time The datetime the corresponds with given position x
  5089. */
  5090. ItemSet.prototype.toTime = function toTime(x) {
  5091. var conversion = this.conversion;
  5092. return new Date(x / conversion.scale + conversion.offset);
  5093. };
  5094. /**
  5095. * Convert a datetime (Date object) into a position on the screen
  5096. * Before this method can be used, the method _updateConversion must be
  5097. * executed once.
  5098. * @param {Date} time A date
  5099. * @return {int} x The position on the screen in pixels which corresponds
  5100. * with the given date.
  5101. */
  5102. ItemSet.prototype.toScreen = function toScreen(time) {
  5103. var conversion = this.conversion;
  5104. return (time.valueOf() - conversion.offset) * conversion.scale;
  5105. };
  5106. /**
  5107. * Start dragging the selected events
  5108. * @param {Event} event
  5109. * @private
  5110. */
  5111. ItemSet.prototype._onDragStart = function (event) {
  5112. if (!this.options.editable) {
  5113. return;
  5114. }
  5115. var item = ItemSet.itemFromTarget(event),
  5116. me = this;
  5117. if (item && item.selected) {
  5118. var dragLeftItem = event.target.dragLeftItem;
  5119. var dragRightItem = event.target.dragRightItem;
  5120. if (dragLeftItem) {
  5121. this.touchParams.itemProps = [{
  5122. item: dragLeftItem,
  5123. start: item.data.start.valueOf()
  5124. }];
  5125. }
  5126. else if (dragRightItem) {
  5127. this.touchParams.itemProps = [{
  5128. item: dragRightItem,
  5129. end: item.data.end.valueOf()
  5130. }];
  5131. }
  5132. else {
  5133. this.touchParams.itemProps = this.getSelection().map(function (id) {
  5134. var item = me.items[id];
  5135. var props = {
  5136. item: item
  5137. };
  5138. if ('start' in item.data) {
  5139. props.start = item.data.start.valueOf()
  5140. }
  5141. if ('end' in item.data) {
  5142. props.end = item.data.end.valueOf()
  5143. }
  5144. return props;
  5145. });
  5146. }
  5147. event.stopPropagation();
  5148. }
  5149. };
  5150. /**
  5151. * Drag selected items
  5152. * @param {Event} event
  5153. * @private
  5154. */
  5155. ItemSet.prototype._onDrag = function (event) {
  5156. if (this.touchParams.itemProps) {
  5157. var snap = this.options.snap || null,
  5158. deltaX = event.gesture.deltaX,
  5159. offset = deltaX / this.conversion.scale;
  5160. // move
  5161. this.touchParams.itemProps.forEach(function (props) {
  5162. if ('start' in props) {
  5163. var start = new Date(props.start + offset);
  5164. props.item.data.start = snap ? snap(start) : start;
  5165. }
  5166. if ('end' in props) {
  5167. var end = new Date(props.end + offset);
  5168. props.item.data.end = snap ? snap(end) : end;
  5169. }
  5170. });
  5171. // TODO: implement onMoving handler
  5172. // TODO: implement dragging from one group to another
  5173. this.requestReflow();
  5174. event.stopPropagation();
  5175. }
  5176. };
  5177. /**
  5178. * End of dragging selected items
  5179. * @param {Event} event
  5180. * @private
  5181. */
  5182. ItemSet.prototype._onDragEnd = function (event) {
  5183. if (this.touchParams.itemProps) {
  5184. // prepare a change set for the changed items
  5185. var changes = [],
  5186. me = this,
  5187. dataset = this._myDataSet(),
  5188. type;
  5189. this.touchParams.itemProps.forEach(function (props) {
  5190. var id = props.item.id,
  5191. item = me.itemsData.get(id);
  5192. var changed = false;
  5193. if ('start' in props.item.data) {
  5194. changed = (props.start != props.item.data.start.valueOf());
  5195. item.start = util.convert(props.item.data.start, dataset.convert['start']);
  5196. }
  5197. if ('end' in props.item.data) {
  5198. changed = changed || (props.end != props.item.data.end.valueOf());
  5199. item.end = util.convert(props.item.data.end, dataset.convert['end']);
  5200. }
  5201. // only apply changes when start or end is actually changed
  5202. if (changed) {
  5203. me.options.onMove(item, function (item) {
  5204. if (item) {
  5205. // apply changes
  5206. changes.push(item);
  5207. }
  5208. else {
  5209. // restore original values
  5210. if ('start' in props) props.item.data.start = props.start;
  5211. if ('end' in props) props.item.data.end = props.end;
  5212. me.requestReflow();
  5213. }
  5214. });
  5215. }
  5216. });
  5217. this.touchParams.itemProps = null;
  5218. // apply the changes to the data (if there are changes)
  5219. if (changes.length) {
  5220. dataset.update(changes);
  5221. }
  5222. event.stopPropagation();
  5223. }
  5224. };
  5225. /**
  5226. * Find an item from an event target:
  5227. * searches for the attribute 'timeline-item' in the event target's element tree
  5228. * @param {Event} event
  5229. * @return {Item | null} item
  5230. */
  5231. ItemSet.itemFromTarget = function itemFromTarget (event) {
  5232. var target = event.target;
  5233. while (target) {
  5234. if (target.hasOwnProperty('timeline-item')) {
  5235. return target['timeline-item'];
  5236. }
  5237. target = target.parentNode;
  5238. }
  5239. return null;
  5240. };
  5241. /**
  5242. * Find the ItemSet from an event target:
  5243. * searches for the attribute 'timeline-itemset' in the event target's element tree
  5244. * @param {Event} event
  5245. * @return {ItemSet | null} item
  5246. */
  5247. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  5248. var target = event.target;
  5249. while (target) {
  5250. if (target.hasOwnProperty('timeline-itemset')) {
  5251. return target['timeline-itemset'];
  5252. }
  5253. target = target.parentNode;
  5254. }
  5255. return null;
  5256. };
  5257. /**
  5258. * Find the DataSet to which this ItemSet is connected
  5259. * @returns {null | DataSet} dataset
  5260. * @private
  5261. */
  5262. ItemSet.prototype._myDataSet = function _myDataSet() {
  5263. // find the root DataSet
  5264. var dataset = this.itemsData;
  5265. while (dataset instanceof DataView) {
  5266. dataset = dataset.data;
  5267. }
  5268. return dataset;
  5269. };
  5270. /**
  5271. * @constructor Item
  5272. * @param {ItemSet} parent
  5273. * @param {Object} data Object containing (optional) parameters type,
  5274. * start, end, content, group, className.
  5275. * @param {Object} [options] Options to set initial property values
  5276. * @param {Object} [defaultOptions] default options
  5277. * // TODO: describe available options
  5278. */
  5279. function Item (parent, data, options, defaultOptions) {
  5280. this.parent = parent;
  5281. this.data = data;
  5282. this.dom = null;
  5283. this.options = options || {};
  5284. this.defaultOptions = defaultOptions || {};
  5285. this.selected = false;
  5286. this.visible = false;
  5287. this.top = 0;
  5288. this.left = 0;
  5289. this.width = 0;
  5290. this.height = 0;
  5291. this.offset = 0;
  5292. }
  5293. /**
  5294. * Select current item
  5295. */
  5296. Item.prototype.select = function select() {
  5297. this.selected = true;
  5298. if (this.visible) this.repaint();
  5299. };
  5300. /**
  5301. * Unselect current item
  5302. */
  5303. Item.prototype.unselect = function unselect() {
  5304. this.selected = false;
  5305. if (this.visible) this.repaint();
  5306. };
  5307. /**
  5308. * Show the Item in the DOM (when not already visible)
  5309. * @return {Boolean} changed
  5310. */
  5311. Item.prototype.show = function show() {
  5312. return false;
  5313. };
  5314. /**
  5315. * Hide the Item from the DOM (when visible)
  5316. * @return {Boolean} changed
  5317. */
  5318. Item.prototype.hide = function hide() {
  5319. return false;
  5320. };
  5321. /**
  5322. * Repaint the item
  5323. * @return {Boolean} changed
  5324. */
  5325. Item.prototype.repaint = function repaint() {
  5326. // should be implemented by the item
  5327. return false;
  5328. };
  5329. /**
  5330. * Reflow the item
  5331. * @return {Boolean} resized
  5332. */
  5333. Item.prototype.reflow = function reflow() {
  5334. // should be implemented by the item
  5335. return false;
  5336. };
  5337. /**
  5338. * Give the item a display offset in pixels
  5339. * @param {Number} offset Offset on screen in pixels
  5340. */
  5341. Item.prototype.setOffset = function setOffset(offset) {
  5342. this.offset = offset;
  5343. };
  5344. /**
  5345. * Repaint a delete button on the top right of the item when the item is selected
  5346. * @param {HTMLElement} anchor
  5347. * @private
  5348. */
  5349. Item.prototype._repaintDeleteButton = function (anchor) {
  5350. if (this.selected && this.options.editable && !this.dom.deleteButton) {
  5351. // create and show button
  5352. var parent = this.parent;
  5353. var id = this.id;
  5354. var deleteButton = document.createElement('div');
  5355. deleteButton.className = 'delete';
  5356. deleteButton.title = 'Delete this item';
  5357. Hammer(deleteButton, {
  5358. preventDefault: true
  5359. }).on('tap', function (event) {
  5360. parent.removeItem(id);
  5361. event.stopPropagation();
  5362. });
  5363. anchor.appendChild(deleteButton);
  5364. this.dom.deleteButton = deleteButton;
  5365. }
  5366. else if (!this.selected && this.dom.deleteButton) {
  5367. // remove button
  5368. if (this.dom.deleteButton.parentNode) {
  5369. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  5370. }
  5371. this.dom.deleteButton = null;
  5372. }
  5373. };
  5374. /**
  5375. * @constructor ItemBox
  5376. * @extends Item
  5377. * @param {ItemSet} parent
  5378. * @param {Object} data Object containing parameters start
  5379. * content, className.
  5380. * @param {Object} [options] Options to set initial property values
  5381. * @param {Object} [defaultOptions] default options
  5382. * // TODO: describe available options
  5383. */
  5384. function ItemBox (parent, data, options, defaultOptions) {
  5385. this.props = {
  5386. dot: {
  5387. left: 0,
  5388. top: 0,
  5389. width: 0,
  5390. height: 0
  5391. },
  5392. line: {
  5393. top: 0,
  5394. left: 0,
  5395. width: 0,
  5396. height: 0
  5397. }
  5398. };
  5399. Item.call(this, parent, data, options, defaultOptions);
  5400. }
  5401. ItemBox.prototype = new Item (null, null);
  5402. /**
  5403. * Repaint the item
  5404. * @return {Boolean} changed
  5405. */
  5406. ItemBox.prototype.repaint = function repaint() {
  5407. // TODO: make an efficient repaint
  5408. var changed = false;
  5409. var dom = this.dom;
  5410. if (!dom) {
  5411. this._create();
  5412. dom = this.dom;
  5413. changed = true;
  5414. }
  5415. if (dom) {
  5416. if (!this.parent) {
  5417. throw new Error('Cannot repaint item: no parent attached');
  5418. }
  5419. if (!dom.box.parentNode) {
  5420. var foreground = this.parent.getForeground();
  5421. if (!foreground) {
  5422. throw new Error('Cannot repaint time axis: ' +
  5423. 'parent has no foreground container element');
  5424. }
  5425. foreground.appendChild(dom.box);
  5426. changed = true;
  5427. }
  5428. if (!dom.line.parentNode) {
  5429. var background = this.parent.getBackground();
  5430. if (!background) {
  5431. throw new Error('Cannot repaint time axis: ' +
  5432. 'parent has no background container element');
  5433. }
  5434. background.appendChild(dom.line);
  5435. changed = true;
  5436. }
  5437. if (!dom.dot.parentNode) {
  5438. var axis = this.parent.getAxis();
  5439. if (!background) {
  5440. throw new Error('Cannot repaint time axis: ' +
  5441. 'parent has no axis container element');
  5442. }
  5443. axis.appendChild(dom.dot);
  5444. changed = true;
  5445. }
  5446. this._repaintDeleteButton(dom.box);
  5447. // update contents
  5448. if (this.data.content != this.content) {
  5449. this.content = this.data.content;
  5450. if (this.content instanceof Element) {
  5451. dom.content.innerHTML = '';
  5452. dom.content.appendChild(this.content);
  5453. }
  5454. else if (this.data.content != undefined) {
  5455. dom.content.innerHTML = this.content;
  5456. }
  5457. else {
  5458. throw new Error('Property "content" missing in item ' + this.data.id);
  5459. }
  5460. changed = true;
  5461. }
  5462. // update class
  5463. var className = (this.data.className? ' ' + this.data.className : '') +
  5464. (this.selected ? ' selected' : '');
  5465. if (this.className != className) {
  5466. this.className = className;
  5467. dom.box.className = 'item box' + className;
  5468. dom.line.className = 'item line' + className;
  5469. dom.dot.className = 'item dot' + className;
  5470. changed = true;
  5471. }
  5472. }
  5473. return changed;
  5474. };
  5475. /**
  5476. * Show the item in the DOM (when not already visible). The items DOM will
  5477. * be created when needed.
  5478. * @return {Boolean} changed
  5479. */
  5480. ItemBox.prototype.show = function show() {
  5481. if (!this.dom || !this.dom.box.parentNode) {
  5482. return this.repaint();
  5483. }
  5484. else {
  5485. return false;
  5486. }
  5487. };
  5488. /**
  5489. * Hide the item from the DOM (when visible)
  5490. * @return {Boolean} changed
  5491. */
  5492. ItemBox.prototype.hide = function hide() {
  5493. var changed = false,
  5494. dom = this.dom;
  5495. if (dom) {
  5496. if (dom.box.parentNode) {
  5497. dom.box.parentNode.removeChild(dom.box);
  5498. changed = true;
  5499. }
  5500. if (dom.line.parentNode) {
  5501. dom.line.parentNode.removeChild(dom.line);
  5502. }
  5503. if (dom.dot.parentNode) {
  5504. dom.dot.parentNode.removeChild(dom.dot);
  5505. }
  5506. }
  5507. return changed;
  5508. };
  5509. /**
  5510. * Reflow the item: calculate its actual size and position from the DOM
  5511. * @return {boolean} resized returns true if the axis is resized
  5512. * @override
  5513. */
  5514. ItemBox.prototype.reflow = function reflow() {
  5515. var changed = 0,
  5516. update,
  5517. dom,
  5518. props,
  5519. options,
  5520. margin,
  5521. start,
  5522. align,
  5523. orientation,
  5524. top,
  5525. left,
  5526. data,
  5527. range;
  5528. if (this.data.start == undefined) {
  5529. throw new Error('Property "start" missing in item ' + this.data.id);
  5530. }
  5531. data = this.data;
  5532. range = this.parent && this.parent.range;
  5533. if (data && range) {
  5534. // TODO: account for the width of the item
  5535. var interval = (range.end - range.start);
  5536. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  5537. }
  5538. else {
  5539. this.visible = false;
  5540. }
  5541. if (this.visible) {
  5542. dom = this.dom;
  5543. if (dom) {
  5544. update = util.updateProperty;
  5545. props = this.props;
  5546. options = this.options;
  5547. start = this.parent.toScreen(this.data.start) + this.offset;
  5548. align = options.align || this.defaultOptions.align;
  5549. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5550. orientation = options.orientation || this.defaultOptions.orientation;
  5551. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5552. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5553. changed += update(props.line, 'width', dom.line.offsetWidth);
  5554. changed += update(props.line, 'height', dom.line.offsetHeight);
  5555. changed += update(props.line, 'top', dom.line.offsetTop);
  5556. changed += update(this, 'width', dom.box.offsetWidth);
  5557. changed += update(this, 'height', dom.box.offsetHeight);
  5558. if (align == 'right') {
  5559. left = start - this.width;
  5560. }
  5561. else if (align == 'left') {
  5562. left = start;
  5563. }
  5564. else {
  5565. // default or 'center'
  5566. left = start - this.width / 2;
  5567. }
  5568. changed += update(this, 'left', left);
  5569. changed += update(props.line, 'left', start - props.line.width / 2);
  5570. changed += update(props.dot, 'left', start - props.dot.width / 2);
  5571. changed += update(props.dot, 'top', -props.dot.height / 2);
  5572. if (orientation == 'top') {
  5573. top = margin;
  5574. changed += update(this, 'top', top);
  5575. }
  5576. else {
  5577. // default or 'bottom'
  5578. var parentHeight = this.parent.height;
  5579. top = parentHeight - this.height - margin;
  5580. changed += update(this, 'top', top);
  5581. }
  5582. }
  5583. else {
  5584. changed += 1;
  5585. }
  5586. }
  5587. return (changed > 0);
  5588. };
  5589. /**
  5590. * Create an items DOM
  5591. * @private
  5592. */
  5593. ItemBox.prototype._create = function _create() {
  5594. var dom = this.dom;
  5595. if (!dom) {
  5596. this.dom = dom = {};
  5597. // create the box
  5598. dom.box = document.createElement('DIV');
  5599. // className is updated in repaint()
  5600. // contents box (inside the background box). used for making margins
  5601. dom.content = document.createElement('DIV');
  5602. dom.content.className = 'content';
  5603. dom.box.appendChild(dom.content);
  5604. // line to axis
  5605. dom.line = document.createElement('DIV');
  5606. dom.line.className = 'line';
  5607. // dot on axis
  5608. dom.dot = document.createElement('DIV');
  5609. dom.dot.className = 'dot';
  5610. // attach this item as attribute
  5611. dom.box['timeline-item'] = this;
  5612. }
  5613. };
  5614. /**
  5615. * Reposition the item, recalculate its left, top, and width, using the current
  5616. * range and size of the items itemset
  5617. * @override
  5618. */
  5619. ItemBox.prototype.reposition = function reposition() {
  5620. var dom = this.dom,
  5621. props = this.props,
  5622. orientation = this.options.orientation || this.defaultOptions.orientation;
  5623. if (dom) {
  5624. var box = dom.box,
  5625. line = dom.line,
  5626. dot = dom.dot;
  5627. box.style.left = this.left + 'px';
  5628. box.style.top = this.top + 'px';
  5629. line.style.left = props.line.left + 'px';
  5630. if (orientation == 'top') {
  5631. line.style.top = 0 + 'px';
  5632. line.style.height = this.top + 'px';
  5633. }
  5634. else {
  5635. // orientation 'bottom'
  5636. line.style.top = (this.top + this.height) + 'px';
  5637. line.style.height = Math.max(this.parent.height - this.top - this.height +
  5638. this.props.dot.height / 2, 0) + 'px';
  5639. }
  5640. dot.style.left = props.dot.left + 'px';
  5641. dot.style.top = props.dot.top + 'px';
  5642. }
  5643. };
  5644. /**
  5645. * @constructor ItemPoint
  5646. * @extends Item
  5647. * @param {ItemSet} parent
  5648. * @param {Object} data Object containing parameters start
  5649. * content, className.
  5650. * @param {Object} [options] Options to set initial property values
  5651. * @param {Object} [defaultOptions] default options
  5652. * // TODO: describe available options
  5653. */
  5654. function ItemPoint (parent, data, options, defaultOptions) {
  5655. this.props = {
  5656. dot: {
  5657. top: 0,
  5658. width: 0,
  5659. height: 0
  5660. },
  5661. content: {
  5662. height: 0,
  5663. marginLeft: 0
  5664. }
  5665. };
  5666. Item.call(this, parent, data, options, defaultOptions);
  5667. }
  5668. ItemPoint.prototype = new Item (null, null);
  5669. /**
  5670. * Repaint the item
  5671. * @return {Boolean} changed
  5672. */
  5673. ItemPoint.prototype.repaint = function repaint() {
  5674. // TODO: make an efficient repaint
  5675. var changed = false;
  5676. var dom = this.dom;
  5677. if (!dom) {
  5678. this._create();
  5679. dom = this.dom;
  5680. changed = true;
  5681. }
  5682. if (dom) {
  5683. if (!this.parent) {
  5684. throw new Error('Cannot repaint item: no parent attached');
  5685. }
  5686. var foreground = this.parent.getForeground();
  5687. if (!foreground) {
  5688. throw new Error('Cannot repaint time axis: ' +
  5689. 'parent has no foreground container element');
  5690. }
  5691. if (!dom.point.parentNode) {
  5692. foreground.appendChild(dom.point);
  5693. foreground.appendChild(dom.point);
  5694. changed = true;
  5695. }
  5696. // update contents
  5697. if (this.data.content != this.content) {
  5698. this.content = this.data.content;
  5699. if (this.content instanceof Element) {
  5700. dom.content.innerHTML = '';
  5701. dom.content.appendChild(this.content);
  5702. }
  5703. else if (this.data.content != undefined) {
  5704. dom.content.innerHTML = this.content;
  5705. }
  5706. else {
  5707. throw new Error('Property "content" missing in item ' + this.data.id);
  5708. }
  5709. changed = true;
  5710. }
  5711. this._repaintDeleteButton(dom.point);
  5712. // update class
  5713. var className = (this.data.className? ' ' + this.data.className : '') +
  5714. (this.selected ? ' selected' : '');
  5715. if (this.className != className) {
  5716. this.className = className;
  5717. dom.point.className = 'item point' + className;
  5718. changed = true;
  5719. }
  5720. }
  5721. return changed;
  5722. };
  5723. /**
  5724. * Show the item in the DOM (when not already visible). The items DOM will
  5725. * be created when needed.
  5726. * @return {Boolean} changed
  5727. */
  5728. ItemPoint.prototype.show = function show() {
  5729. if (!this.dom || !this.dom.point.parentNode) {
  5730. return this.repaint();
  5731. }
  5732. else {
  5733. return false;
  5734. }
  5735. };
  5736. /**
  5737. * Hide the item from the DOM (when visible)
  5738. * @return {Boolean} changed
  5739. */
  5740. ItemPoint.prototype.hide = function hide() {
  5741. var changed = false,
  5742. dom = this.dom;
  5743. if (dom) {
  5744. if (dom.point.parentNode) {
  5745. dom.point.parentNode.removeChild(dom.point);
  5746. changed = true;
  5747. }
  5748. }
  5749. return changed;
  5750. };
  5751. /**
  5752. * Reflow the item: calculate its actual size from the DOM
  5753. * @return {boolean} resized returns true if the axis is resized
  5754. * @override
  5755. */
  5756. ItemPoint.prototype.reflow = function reflow() {
  5757. var changed = 0,
  5758. update,
  5759. dom,
  5760. props,
  5761. options,
  5762. margin,
  5763. orientation,
  5764. start,
  5765. top,
  5766. data,
  5767. range;
  5768. if (this.data.start == undefined) {
  5769. throw new Error('Property "start" missing in item ' + this.data.id);
  5770. }
  5771. data = this.data;
  5772. range = this.parent && this.parent.range;
  5773. if (data && range) {
  5774. // TODO: account for the width of the item
  5775. var interval = (range.end - range.start);
  5776. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  5777. }
  5778. else {
  5779. this.visible = false;
  5780. }
  5781. if (this.visible) {
  5782. dom = this.dom;
  5783. if (dom) {
  5784. update = util.updateProperty;
  5785. props = this.props;
  5786. options = this.options;
  5787. orientation = options.orientation || this.defaultOptions.orientation;
  5788. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5789. start = this.parent.toScreen(this.data.start) + this.offset;
  5790. changed += update(this, 'width', dom.point.offsetWidth);
  5791. changed += update(this, 'height', dom.point.offsetHeight);
  5792. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5793. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5794. changed += update(props.content, 'height', dom.content.offsetHeight);
  5795. if (orientation == 'top') {
  5796. top = margin;
  5797. }
  5798. else {
  5799. // default or 'bottom'
  5800. var parentHeight = this.parent.height;
  5801. top = Math.max(parentHeight - this.height - margin, 0);
  5802. }
  5803. changed += update(this, 'top', top);
  5804. changed += update(this, 'left', start - props.dot.width / 2);
  5805. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5806. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5807. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5808. }
  5809. else {
  5810. changed += 1;
  5811. }
  5812. }
  5813. return (changed > 0);
  5814. };
  5815. /**
  5816. * Create an items DOM
  5817. * @private
  5818. */
  5819. ItemPoint.prototype._create = function _create() {
  5820. var dom = this.dom;
  5821. if (!dom) {
  5822. this.dom = dom = {};
  5823. // background box
  5824. dom.point = document.createElement('div');
  5825. // className is updated in repaint()
  5826. // contents box, right from the dot
  5827. dom.content = document.createElement('div');
  5828. dom.content.className = 'content';
  5829. dom.point.appendChild(dom.content);
  5830. // dot at start
  5831. dom.dot = document.createElement('div');
  5832. dom.dot.className = 'dot';
  5833. dom.point.appendChild(dom.dot);
  5834. // attach this item as attribute
  5835. dom.point['timeline-item'] = this;
  5836. }
  5837. };
  5838. /**
  5839. * Reposition the item, recalculate its left, top, and width, using the current
  5840. * range and size of the items itemset
  5841. * @override
  5842. */
  5843. ItemPoint.prototype.reposition = function reposition() {
  5844. var dom = this.dom,
  5845. props = this.props;
  5846. if (dom) {
  5847. dom.point.style.top = this.top + 'px';
  5848. dom.point.style.left = this.left + 'px';
  5849. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5850. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5851. dom.dot.style.top = props.dot.top + 'px';
  5852. }
  5853. };
  5854. /**
  5855. * @constructor ItemRange
  5856. * @extends Item
  5857. * @param {ItemSet} parent
  5858. * @param {Object} data Object containing parameters start, end
  5859. * content, className.
  5860. * @param {Object} [options] Options to set initial property values
  5861. * @param {Object} [defaultOptions] default options
  5862. * // TODO: describe available options
  5863. */
  5864. function ItemRange (parent, data, options, defaultOptions) {
  5865. this.props = {
  5866. content: {
  5867. left: 0,
  5868. width: 0
  5869. }
  5870. };
  5871. Item.call(this, parent, data, options, defaultOptions);
  5872. }
  5873. ItemRange.prototype = new Item (null, null);
  5874. /**
  5875. * Repaint the item
  5876. * @return {Boolean} changed
  5877. */
  5878. ItemRange.prototype.repaint = function repaint() {
  5879. // TODO: make an efficient repaint
  5880. var changed = false;
  5881. var dom = this.dom;
  5882. if (!dom) {
  5883. this._create();
  5884. dom = this.dom;
  5885. changed = true;
  5886. }
  5887. if (dom) {
  5888. if (!this.parent) {
  5889. throw new Error('Cannot repaint item: no parent attached');
  5890. }
  5891. var foreground = this.parent.getForeground();
  5892. if (!foreground) {
  5893. throw new Error('Cannot repaint time axis: ' +
  5894. 'parent has no foreground container element');
  5895. }
  5896. if (!dom.box.parentNode) {
  5897. foreground.appendChild(dom.box);
  5898. changed = true;
  5899. }
  5900. // update content
  5901. if (this.data.content != this.content) {
  5902. this.content = this.data.content;
  5903. if (this.content instanceof Element) {
  5904. dom.content.innerHTML = '';
  5905. dom.content.appendChild(this.content);
  5906. }
  5907. else if (this.data.content != undefined) {
  5908. dom.content.innerHTML = this.content;
  5909. }
  5910. else {
  5911. throw new Error('Property "content" missing in item ' + this.data.id);
  5912. }
  5913. changed = true;
  5914. }
  5915. this._repaintDeleteButton(dom.box);
  5916. this._repaintDragLeft();
  5917. this._repaintDragRight();
  5918. // update class
  5919. var className = (this.data.className ? (' ' + this.data.className) : '') +
  5920. (this.selected ? ' selected' : '');
  5921. if (this.className != className) {
  5922. this.className = className;
  5923. dom.box.className = 'item range' + className;
  5924. changed = true;
  5925. }
  5926. }
  5927. return changed;
  5928. };
  5929. /**
  5930. * Show the item in the DOM (when not already visible). The items DOM will
  5931. * be created when needed.
  5932. * @return {Boolean} changed
  5933. */
  5934. ItemRange.prototype.show = function show() {
  5935. if (!this.dom || !this.dom.box.parentNode) {
  5936. return this.repaint();
  5937. }
  5938. else {
  5939. return false;
  5940. }
  5941. };
  5942. /**
  5943. * Hide the item from the DOM (when visible)
  5944. * @return {Boolean} changed
  5945. */
  5946. ItemRange.prototype.hide = function hide() {
  5947. var changed = false,
  5948. dom = this.dom;
  5949. if (dom) {
  5950. if (dom.box.parentNode) {
  5951. dom.box.parentNode.removeChild(dom.box);
  5952. changed = true;
  5953. }
  5954. }
  5955. return changed;
  5956. };
  5957. /**
  5958. * Reflow the item: calculate its actual size from the DOM
  5959. * @return {boolean} resized returns true if the axis is resized
  5960. * @override
  5961. */
  5962. ItemRange.prototype.reflow = function reflow() {
  5963. var changed = 0,
  5964. dom,
  5965. props,
  5966. options,
  5967. margin,
  5968. padding,
  5969. parent,
  5970. start,
  5971. end,
  5972. data,
  5973. range,
  5974. update,
  5975. box,
  5976. parentWidth,
  5977. contentLeft,
  5978. orientation,
  5979. top;
  5980. if (this.data.start == undefined) {
  5981. throw new Error('Property "start" missing in item ' + this.data.id);
  5982. }
  5983. if (this.data.end == undefined) {
  5984. throw new Error('Property "end" missing in item ' + this.data.id);
  5985. }
  5986. data = this.data;
  5987. range = this.parent && this.parent.range;
  5988. if (data && range) {
  5989. // TODO: account for the width of the item. Take some margin
  5990. this.visible = (data.start < range.end) && (data.end > range.start);
  5991. }
  5992. else {
  5993. this.visible = false;
  5994. }
  5995. if (this.visible) {
  5996. dom = this.dom;
  5997. if (dom) {
  5998. props = this.props;
  5999. options = this.options;
  6000. parent = this.parent;
  6001. start = parent.toScreen(this.data.start) + this.offset;
  6002. end = parent.toScreen(this.data.end) + this.offset;
  6003. update = util.updateProperty;
  6004. box = dom.box;
  6005. parentWidth = parent.width;
  6006. orientation = options.orientation || this.defaultOptions.orientation;
  6007. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  6008. padding = options.padding || this.defaultOptions.padding;
  6009. changed += update(props.content, 'width', dom.content.offsetWidth);
  6010. changed += update(this, 'height', box.offsetHeight);
  6011. // limit the width of the this, as browsers cannot draw very wide divs
  6012. if (start < -parentWidth) {
  6013. start = -parentWidth;
  6014. }
  6015. if (end > 2 * parentWidth) {
  6016. end = 2 * parentWidth;
  6017. }
  6018. // when range exceeds left of the window, position the contents at the left of the visible area
  6019. if (start < 0) {
  6020. contentLeft = Math.min(-start,
  6021. (end - start - props.content.width - 2 * padding));
  6022. // TODO: remove the need for options.padding. it's terrible.
  6023. }
  6024. else {
  6025. contentLeft = 0;
  6026. }
  6027. changed += update(props.content, 'left', contentLeft);
  6028. if (orientation == 'top') {
  6029. top = margin;
  6030. changed += update(this, 'top', top);
  6031. }
  6032. else {
  6033. // default or 'bottom'
  6034. top = parent.height - this.height - margin;
  6035. changed += update(this, 'top', top);
  6036. }
  6037. changed += update(this, 'left', start);
  6038. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  6039. }
  6040. else {
  6041. changed += 1;
  6042. }
  6043. }
  6044. return (changed > 0);
  6045. };
  6046. /**
  6047. * Create an items DOM
  6048. * @private
  6049. */
  6050. ItemRange.prototype._create = function _create() {
  6051. var dom = this.dom;
  6052. if (!dom) {
  6053. this.dom = dom = {};
  6054. // background box
  6055. dom.box = document.createElement('div');
  6056. // className is updated in repaint()
  6057. // contents box
  6058. dom.content = document.createElement('div');
  6059. dom.content.className = 'content';
  6060. dom.box.appendChild(dom.content);
  6061. // attach this item as attribute
  6062. dom.box['timeline-item'] = this;
  6063. }
  6064. };
  6065. /**
  6066. * Reposition the item, recalculate its left, top, and width, using the current
  6067. * range and size of the items itemset
  6068. * @override
  6069. */
  6070. ItemRange.prototype.reposition = function reposition() {
  6071. var dom = this.dom,
  6072. props = this.props;
  6073. if (dom) {
  6074. dom.box.style.top = this.top + 'px';
  6075. dom.box.style.left = this.left + 'px';
  6076. dom.box.style.width = this.width + 'px';
  6077. dom.content.style.left = props.content.left + 'px';
  6078. }
  6079. };
  6080. /**
  6081. * Repaint a drag area on the left side of the range when the range is selected
  6082. * @private
  6083. */
  6084. ItemRange.prototype._repaintDragLeft = function () {
  6085. if (this.selected && this.options.editable && !this.dom.dragLeft) {
  6086. // create and show drag area
  6087. var dragLeft = document.createElement('div');
  6088. dragLeft.className = 'drag-left';
  6089. dragLeft.dragLeftItem = this;
  6090. // TODO: this should be redundant?
  6091. Hammer(dragLeft, {
  6092. preventDefault: true
  6093. }).on('drag', function () {
  6094. //console.log('drag left')
  6095. });
  6096. this.dom.box.appendChild(dragLeft);
  6097. this.dom.dragLeft = dragLeft;
  6098. }
  6099. else if (!this.selected && this.dom.dragLeft) {
  6100. // delete drag area
  6101. if (this.dom.dragLeft.parentNode) {
  6102. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  6103. }
  6104. this.dom.dragLeft = null;
  6105. }
  6106. };
  6107. /**
  6108. * Repaint a drag area on the right side of the range when the range is selected
  6109. * @private
  6110. */
  6111. ItemRange.prototype._repaintDragRight = function () {
  6112. if (this.selected && this.options.editable && !this.dom.dragRight) {
  6113. // create and show drag area
  6114. var dragRight = document.createElement('div');
  6115. dragRight.className = 'drag-right';
  6116. dragRight.dragRightItem = this;
  6117. // TODO: this should be redundant?
  6118. Hammer(dragRight, {
  6119. preventDefault: true
  6120. }).on('drag', function () {
  6121. //console.log('drag right')
  6122. });
  6123. this.dom.box.appendChild(dragRight);
  6124. this.dom.dragRight = dragRight;
  6125. }
  6126. else if (!this.selected && this.dom.dragRight) {
  6127. // delete drag area
  6128. if (this.dom.dragRight.parentNode) {
  6129. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  6130. }
  6131. this.dom.dragRight = null;
  6132. }
  6133. };
  6134. /**
  6135. * @constructor ItemRangeOverflow
  6136. * @extends ItemRange
  6137. * @param {ItemSet} parent
  6138. * @param {Object} data Object containing parameters start, end
  6139. * content, className.
  6140. * @param {Object} [options] Options to set initial property values
  6141. * @param {Object} [defaultOptions] default options
  6142. * // TODO: describe available options
  6143. */
  6144. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  6145. this.props = {
  6146. content: {
  6147. left: 0,
  6148. width: 0
  6149. }
  6150. };
  6151. // define a private property _width, which is the with of the range box
  6152. // adhering to the ranges start and end date. The property width has a
  6153. // getter which returns the max of border width and content width
  6154. this._width = 0;
  6155. Object.defineProperty(this, 'width', {
  6156. get: function () {
  6157. return (this.props.content && this._width < this.props.content.width) ?
  6158. this.props.content.width :
  6159. this._width;
  6160. },
  6161. set: function (width) {
  6162. this._width = width;
  6163. }
  6164. });
  6165. ItemRange.call(this, parent, data, options, defaultOptions);
  6166. }
  6167. ItemRangeOverflow.prototype = new ItemRange (null, null);
  6168. /**
  6169. * Repaint the item
  6170. * @return {Boolean} changed
  6171. */
  6172. ItemRangeOverflow.prototype.repaint = function repaint() {
  6173. // TODO: make an efficient repaint
  6174. var changed = false;
  6175. var dom = this.dom;
  6176. if (!dom) {
  6177. this._create();
  6178. dom = this.dom;
  6179. changed = true;
  6180. }
  6181. if (dom) {
  6182. if (!this.parent) {
  6183. throw new Error('Cannot repaint item: no parent attached');
  6184. }
  6185. var foreground = this.parent.getForeground();
  6186. if (!foreground) {
  6187. throw new Error('Cannot repaint time axis: ' +
  6188. 'parent has no foreground container element');
  6189. }
  6190. if (!dom.box.parentNode) {
  6191. foreground.appendChild(dom.box);
  6192. changed = true;
  6193. }
  6194. // update content
  6195. if (this.data.content != this.content) {
  6196. this.content = this.data.content;
  6197. if (this.content instanceof Element) {
  6198. dom.content.innerHTML = '';
  6199. dom.content.appendChild(this.content);
  6200. }
  6201. else if (this.data.content != undefined) {
  6202. dom.content.innerHTML = this.content;
  6203. }
  6204. else {
  6205. throw new Error('Property "content" missing in item ' + this.id);
  6206. }
  6207. changed = true;
  6208. }
  6209. this._repaintDeleteButton(dom.box);
  6210. this._repaintDragLeft();
  6211. this._repaintDragRight();
  6212. // update class
  6213. var className = (this.data.className? ' ' + this.data.className : '') +
  6214. (this.selected ? ' selected' : '');
  6215. if (this.className != className) {
  6216. this.className = className;
  6217. dom.box.className = 'item rangeoverflow' + className;
  6218. changed = true;
  6219. }
  6220. }
  6221. return changed;
  6222. };
  6223. /**
  6224. * Reposition the item, recalculate its left, top, and width, using the current
  6225. * range and size of the items itemset
  6226. * @override
  6227. */
  6228. ItemRangeOverflow.prototype.reposition = function reposition() {
  6229. var dom = this.dom,
  6230. props = this.props;
  6231. if (dom) {
  6232. dom.box.style.top = this.top + 'px';
  6233. dom.box.style.left = this.left + 'px';
  6234. dom.box.style.width = this._width + 'px';
  6235. dom.content.style.left = props.content.left + 'px';
  6236. }
  6237. };
  6238. /**
  6239. * @constructor Group
  6240. * @param {GroupSet} parent
  6241. * @param {Number | String} groupId
  6242. * @param {Object} [options] Options to set initial property values
  6243. * // TODO: describe available options
  6244. * @extends Component
  6245. */
  6246. function Group (parent, groupId, options) {
  6247. this.id = util.randomUUID();
  6248. this.parent = parent;
  6249. this.groupId = groupId;
  6250. this.itemset = null; // ItemSet
  6251. this.options = options || {};
  6252. this.options.top = 0;
  6253. this.props = {
  6254. label: {
  6255. width: 0,
  6256. height: 0
  6257. }
  6258. };
  6259. this.top = 0;
  6260. this.left = 0;
  6261. this.width = 0;
  6262. this.height = 0;
  6263. }
  6264. Group.prototype = new Component();
  6265. // TODO: comment
  6266. Group.prototype.setOptions = Component.prototype.setOptions;
  6267. /**
  6268. * Get the container element of the panel, which can be used by a child to
  6269. * add its own widgets.
  6270. * @returns {HTMLElement} container
  6271. */
  6272. Group.prototype.getContainer = function () {
  6273. return this.parent.getContainer();
  6274. };
  6275. /**
  6276. * Set item set for the group. The group will create a view on the itemset,
  6277. * filtered by the groups id.
  6278. * @param {DataSet | DataView} items
  6279. */
  6280. Group.prototype.setItems = function setItems(items) {
  6281. if (this.itemset) {
  6282. // remove current item set
  6283. this.itemset.hide();
  6284. this.itemset.setItems();
  6285. this.parent.controller.remove(this.itemset);
  6286. this.itemset = null;
  6287. }
  6288. if (items) {
  6289. var groupId = this.groupId;
  6290. var itemsetOptions = Object.create(this.options);
  6291. this.itemset = new ItemSet(this, null, itemsetOptions);
  6292. this.itemset.setRange(this.parent.range);
  6293. this.view = new DataView(items, {
  6294. filter: function (item) {
  6295. return item.group == groupId;
  6296. }
  6297. });
  6298. this.itemset.setItems(this.view);
  6299. this.parent.controller.add(this.itemset);
  6300. }
  6301. };
  6302. /**
  6303. * Set selected items by their id. Replaces the current selection.
  6304. * Unknown id's are silently ignored.
  6305. * @param {Array} [ids] An array with zero or more id's of the items to be
  6306. * selected. If ids is an empty array, all items will be
  6307. * unselected.
  6308. */
  6309. Group.prototype.setSelection = function setSelection(ids) {
  6310. if (this.itemset) this.itemset.setSelection(ids);
  6311. };
  6312. /**
  6313. * Get the selected items by their id
  6314. * @return {Array} ids The ids of the selected items
  6315. */
  6316. Group.prototype.getSelection = function getSelection() {
  6317. return this.itemset ? this.itemset.getSelection() : [];
  6318. };
  6319. /**
  6320. * Repaint the item
  6321. * @return {Boolean} changed
  6322. */
  6323. Group.prototype.repaint = function repaint() {
  6324. return false;
  6325. };
  6326. /**
  6327. * Reflow the item
  6328. * @return {Boolean} resized
  6329. */
  6330. Group.prototype.reflow = function reflow() {
  6331. var changed = 0,
  6332. update = util.updateProperty;
  6333. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  6334. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  6335. // TODO: reckon with the height of the group label
  6336. if (this.label) {
  6337. var inner = this.label.firstChild;
  6338. changed += update(this.props.label, 'width', inner.clientWidth);
  6339. changed += update(this.props.label, 'height', inner.clientHeight);
  6340. }
  6341. else {
  6342. changed += update(this.props.label, 'width', 0);
  6343. changed += update(this.props.label, 'height', 0);
  6344. }
  6345. return (changed > 0);
  6346. };
  6347. /**
  6348. * An GroupSet holds a set of groups
  6349. * @param {Component} parent
  6350. * @param {Component[]} [depends] Components on which this components depends
  6351. * (except for the parent)
  6352. * @param {Object} [options] See GroupSet.setOptions for the available
  6353. * options.
  6354. * @constructor GroupSet
  6355. * @extends Panel
  6356. */
  6357. function GroupSet(parent, depends, options) {
  6358. this.id = util.randomUUID();
  6359. this.parent = parent;
  6360. this.depends = depends;
  6361. this.options = options || {};
  6362. this.range = null; // Range or Object {start: number, end: number}
  6363. this.itemsData = null; // DataSet with items
  6364. this.groupsData = null; // DataSet with groups
  6365. this.groups = {}; // map with groups
  6366. this.dom = {};
  6367. this.props = {
  6368. labels: {
  6369. width: 0
  6370. }
  6371. };
  6372. // TODO: implement right orientation of the labels
  6373. // changes in groups are queued key/value map containing id/action
  6374. this.queue = {};
  6375. var me = this;
  6376. this.listeners = {
  6377. 'add': function (event, params) {
  6378. me._onAdd(params.items);
  6379. },
  6380. 'update': function (event, params) {
  6381. me._onUpdate(params.items);
  6382. },
  6383. 'remove': function (event, params) {
  6384. me._onRemove(params.items);
  6385. }
  6386. };
  6387. }
  6388. GroupSet.prototype = new Panel();
  6389. /**
  6390. * Set options for the GroupSet. Existing options will be extended/overwritten.
  6391. * @param {Object} [options] The following options are available:
  6392. * {String | function} groupsOrder
  6393. * TODO: describe options
  6394. */
  6395. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  6396. GroupSet.prototype.setRange = function (range) {
  6397. // TODO: implement setRange
  6398. };
  6399. /**
  6400. * Set items
  6401. * @param {vis.DataSet | null} items
  6402. */
  6403. GroupSet.prototype.setItems = function setItems(items) {
  6404. this.itemsData = items;
  6405. for (var id in this.groups) {
  6406. if (this.groups.hasOwnProperty(id)) {
  6407. var group = this.groups[id];
  6408. group.setItems(items);
  6409. }
  6410. }
  6411. };
  6412. /**
  6413. * Get items
  6414. * @return {vis.DataSet | null} items
  6415. */
  6416. GroupSet.prototype.getItems = function getItems() {
  6417. return this.itemsData;
  6418. };
  6419. /**
  6420. * Set range (start and end).
  6421. * @param {Range | Object} range A Range or an object containing start and end.
  6422. */
  6423. GroupSet.prototype.setRange = function setRange(range) {
  6424. this.range = range;
  6425. };
  6426. /**
  6427. * Set groups
  6428. * @param {vis.DataSet} groups
  6429. */
  6430. GroupSet.prototype.setGroups = function setGroups(groups) {
  6431. var me = this,
  6432. ids;
  6433. // unsubscribe from current dataset
  6434. if (this.groupsData) {
  6435. util.forEach(this.listeners, function (callback, event) {
  6436. me.groupsData.unsubscribe(event, callback);
  6437. });
  6438. // remove all drawn groups
  6439. ids = this.groupsData.getIds();
  6440. this._onRemove(ids);
  6441. }
  6442. // replace the dataset
  6443. if (!groups) {
  6444. this.groupsData = null;
  6445. }
  6446. else if (groups instanceof DataSet) {
  6447. this.groupsData = groups;
  6448. }
  6449. else {
  6450. this.groupsData = new DataSet({
  6451. convert: {
  6452. start: 'Date',
  6453. end: 'Date'
  6454. }
  6455. });
  6456. this.groupsData.add(groups);
  6457. }
  6458. if (this.groupsData) {
  6459. // subscribe to new dataset
  6460. var id = this.id;
  6461. util.forEach(this.listeners, function (callback, event) {
  6462. me.groupsData.on(event, callback, id);
  6463. });
  6464. // draw all new groups
  6465. ids = this.groupsData.getIds();
  6466. this._onAdd(ids);
  6467. }
  6468. };
  6469. /**
  6470. * Get groups
  6471. * @return {vis.DataSet | null} groups
  6472. */
  6473. GroupSet.prototype.getGroups = function getGroups() {
  6474. return this.groupsData;
  6475. };
  6476. /**
  6477. * Set selected items by their id. Replaces the current selection.
  6478. * Unknown id's are silently ignored.
  6479. * @param {Array} [ids] An array with zero or more id's of the items to be
  6480. * selected. If ids is an empty array, all items will be
  6481. * unselected.
  6482. */
  6483. GroupSet.prototype.setSelection = function setSelection(ids) {
  6484. var selection = [],
  6485. groups = this.groups;
  6486. // iterate over each of the groups
  6487. for (var id in groups) {
  6488. if (groups.hasOwnProperty(id)) {
  6489. var group = groups[id];
  6490. group.setSelection(ids);
  6491. }
  6492. }
  6493. return selection;
  6494. };
  6495. /**
  6496. * Get the selected items by their id
  6497. * @return {Array} ids The ids of the selected items
  6498. */
  6499. GroupSet.prototype.getSelection = function getSelection() {
  6500. var selection = [],
  6501. groups = this.groups;
  6502. // iterate over each of the groups
  6503. for (var id in groups) {
  6504. if (groups.hasOwnProperty(id)) {
  6505. var group = groups[id];
  6506. selection = selection.concat(group.getSelection());
  6507. }
  6508. }
  6509. return selection;
  6510. };
  6511. /**
  6512. * Repaint the component
  6513. * @return {Boolean} changed
  6514. */
  6515. GroupSet.prototype.repaint = function repaint() {
  6516. var changed = 0,
  6517. i, id, group, label,
  6518. update = util.updateProperty,
  6519. asSize = util.option.asSize,
  6520. asElement = util.option.asElement,
  6521. options = this.options,
  6522. frame = this.dom.frame,
  6523. labels = this.dom.labels,
  6524. labelSet = this.dom.labelSet;
  6525. // create frame
  6526. if (!this.parent) {
  6527. throw new Error('Cannot repaint groupset: no parent attached');
  6528. }
  6529. var parentContainer = this.parent.getContainer();
  6530. if (!parentContainer) {
  6531. throw new Error('Cannot repaint groupset: parent has no container element');
  6532. }
  6533. if (!frame) {
  6534. frame = document.createElement('div');
  6535. frame.className = 'groupset';
  6536. frame['timeline-groupset'] = this;
  6537. this.dom.frame = frame;
  6538. var className = options.className;
  6539. if (className) {
  6540. util.addClassName(frame, util.option.asString(className));
  6541. }
  6542. changed += 1;
  6543. }
  6544. if (!frame.parentNode) {
  6545. parentContainer.appendChild(frame);
  6546. changed += 1;
  6547. }
  6548. // create labels
  6549. var labelContainer = asElement(options.labelContainer);
  6550. if (!labelContainer) {
  6551. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  6552. }
  6553. if (!labels) {
  6554. labels = document.createElement('div');
  6555. labels.className = 'labels';
  6556. this.dom.labels = labels;
  6557. }
  6558. if (!labelSet) {
  6559. labelSet = document.createElement('div');
  6560. labelSet.className = 'label-set';
  6561. labels.appendChild(labelSet);
  6562. this.dom.labelSet = labelSet;
  6563. }
  6564. if (!labels.parentNode || labels.parentNode != labelContainer) {
  6565. if (labels.parentNode) {
  6566. labels.parentNode.removeChild(labels.parentNode);
  6567. }
  6568. labelContainer.appendChild(labels);
  6569. }
  6570. // reposition frame
  6571. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  6572. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6573. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6574. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6575. // reposition labels
  6576. changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
  6577. changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
  6578. var me = this,
  6579. queue = this.queue,
  6580. groups = this.groups,
  6581. groupsData = this.groupsData;
  6582. // show/hide added/changed/removed groups
  6583. var ids = Object.keys(queue);
  6584. if (ids.length) {
  6585. ids.forEach(function (id) {
  6586. var action = queue[id];
  6587. var group = groups[id];
  6588. //noinspection FallthroughInSwitchStatementJS
  6589. switch (action) {
  6590. case 'add':
  6591. case 'update':
  6592. if (!group) {
  6593. var groupOptions = Object.create(me.options);
  6594. util.extend(groupOptions, {
  6595. height: null,
  6596. maxHeight: null
  6597. });
  6598. group = new Group(me, id, groupOptions);
  6599. group.setItems(me.itemsData); // attach items data
  6600. groups[id] = group;
  6601. me.controller.add(group);
  6602. }
  6603. // TODO: update group data
  6604. group.data = groupsData.get(id);
  6605. delete queue[id];
  6606. break;
  6607. case 'remove':
  6608. if (group) {
  6609. group.setItems(); // detach items data
  6610. delete groups[id];
  6611. me.controller.remove(group);
  6612. }
  6613. // update lists
  6614. delete queue[id];
  6615. break;
  6616. default:
  6617. console.log('Error: unknown action "' + action + '"');
  6618. }
  6619. });
  6620. // the groupset depends on each of the groups
  6621. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  6622. // TODO: apply dependencies of the groupset
  6623. // update the top positions of the groups in the correct order
  6624. var orderedGroups = this.groupsData.getIds({
  6625. order: this.options.groupOrder
  6626. });
  6627. for (i = 0; i < orderedGroups.length; i++) {
  6628. (function (group, prevGroup) {
  6629. var top = 0;
  6630. if (prevGroup) {
  6631. top = function () {
  6632. // TODO: top must reckon with options.maxHeight
  6633. return prevGroup.top + prevGroup.height;
  6634. }
  6635. }
  6636. group.setOptions({
  6637. top: top
  6638. });
  6639. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  6640. }
  6641. // (re)create the labels
  6642. while (labelSet.firstChild) {
  6643. labelSet.removeChild(labelSet.firstChild);
  6644. }
  6645. for (i = 0; i < orderedGroups.length; i++) {
  6646. id = orderedGroups[i];
  6647. label = this._createLabel(id);
  6648. labelSet.appendChild(label);
  6649. }
  6650. changed++;
  6651. }
  6652. // reposition the labels
  6653. // TODO: labels are not displayed correctly when orientation=='top'
  6654. // TODO: width of labelPanel is not immediately updated on a change in groups
  6655. for (id in groups) {
  6656. if (groups.hasOwnProperty(id)) {
  6657. group = groups[id];
  6658. label = group.label;
  6659. if (label) {
  6660. label.style.top = group.top + 'px';
  6661. label.style.height = group.height + 'px';
  6662. }
  6663. }
  6664. }
  6665. return (changed > 0);
  6666. };
  6667. /**
  6668. * Create a label for group with given id
  6669. * @param {Number} id
  6670. * @return {Element} label
  6671. * @private
  6672. */
  6673. GroupSet.prototype._createLabel = function(id) {
  6674. var group = this.groups[id];
  6675. var label = document.createElement('div');
  6676. label.className = 'vlabel';
  6677. var inner = document.createElement('div');
  6678. inner.className = 'inner';
  6679. label.appendChild(inner);
  6680. var content = group.data && group.data.content;
  6681. if (content instanceof Element) {
  6682. inner.appendChild(content);
  6683. }
  6684. else if (content != undefined) {
  6685. inner.innerHTML = content;
  6686. }
  6687. var className = group.data && group.data.className;
  6688. if (className) {
  6689. util.addClassName(label, className);
  6690. }
  6691. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  6692. return label;
  6693. };
  6694. /**
  6695. * Get container element
  6696. * @return {HTMLElement} container
  6697. */
  6698. GroupSet.prototype.getContainer = function getContainer() {
  6699. return this.dom.frame;
  6700. };
  6701. /**
  6702. * Get the width of the group labels
  6703. * @return {Number} width
  6704. */
  6705. GroupSet.prototype.getLabelsWidth = function getContainer() {
  6706. return this.props.labels.width;
  6707. };
  6708. /**
  6709. * Reflow the component
  6710. * @return {Boolean} resized
  6711. */
  6712. GroupSet.prototype.reflow = function reflow() {
  6713. var changed = 0,
  6714. id, group,
  6715. options = this.options,
  6716. update = util.updateProperty,
  6717. asNumber = util.option.asNumber,
  6718. asSize = util.option.asSize,
  6719. frame = this.dom.frame;
  6720. if (frame) {
  6721. var maxHeight = asNumber(options.maxHeight);
  6722. var fixedHeight = (asSize(options.height) != null);
  6723. var height;
  6724. if (fixedHeight) {
  6725. height = frame.offsetHeight;
  6726. }
  6727. else {
  6728. // height is not specified, calculate the sum of the height of all groups
  6729. height = 0;
  6730. for (id in this.groups) {
  6731. if (this.groups.hasOwnProperty(id)) {
  6732. group = this.groups[id];
  6733. height += group.height;
  6734. }
  6735. }
  6736. }
  6737. if (maxHeight != null) {
  6738. height = Math.min(height, maxHeight);
  6739. }
  6740. changed += update(this, 'height', height);
  6741. changed += update(this, 'top', frame.offsetTop);
  6742. changed += update(this, 'left', frame.offsetLeft);
  6743. changed += update(this, 'width', frame.offsetWidth);
  6744. }
  6745. // calculate the maximum width of the labels
  6746. var width = 0;
  6747. for (id in this.groups) {
  6748. if (this.groups.hasOwnProperty(id)) {
  6749. group = this.groups[id];
  6750. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  6751. width = Math.max(width, labelWidth);
  6752. }
  6753. }
  6754. changed += update(this.props.labels, 'width', width);
  6755. return (changed > 0);
  6756. };
  6757. /**
  6758. * Hide the component from the DOM
  6759. * @return {Boolean} changed
  6760. */
  6761. GroupSet.prototype.hide = function hide() {
  6762. if (this.dom.frame && this.dom.frame.parentNode) {
  6763. this.dom.frame.parentNode.removeChild(this.dom.frame);
  6764. return true;
  6765. }
  6766. else {
  6767. return false;
  6768. }
  6769. };
  6770. /**
  6771. * Show the component in the DOM (when not already visible).
  6772. * A repaint will be executed when the component is not visible
  6773. * @return {Boolean} changed
  6774. */
  6775. GroupSet.prototype.show = function show() {
  6776. if (!this.dom.frame || !this.dom.frame.parentNode) {
  6777. return this.repaint();
  6778. }
  6779. else {
  6780. return false;
  6781. }
  6782. };
  6783. /**
  6784. * Handle updated groups
  6785. * @param {Number[]} ids
  6786. * @private
  6787. */
  6788. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  6789. this._toQueue(ids, 'update');
  6790. };
  6791. /**
  6792. * Handle changed groups
  6793. * @param {Number[]} ids
  6794. * @private
  6795. */
  6796. GroupSet.prototype._onAdd = function _onAdd(ids) {
  6797. this._toQueue(ids, 'add');
  6798. };
  6799. /**
  6800. * Handle removed groups
  6801. * @param {Number[]} ids
  6802. * @private
  6803. */
  6804. GroupSet.prototype._onRemove = function _onRemove(ids) {
  6805. this._toQueue(ids, 'remove');
  6806. };
  6807. /**
  6808. * Put groups in the queue to be added/updated/remove
  6809. * @param {Number[]} ids
  6810. * @param {String} action can be 'add', 'update', 'remove'
  6811. */
  6812. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  6813. var queue = this.queue;
  6814. ids.forEach(function (id) {
  6815. queue[id] = action;
  6816. });
  6817. if (this.controller) {
  6818. //this.requestReflow();
  6819. this.requestRepaint();
  6820. }
  6821. };
  6822. /**
  6823. * Find the Group from an event target:
  6824. * searches for the attribute 'timeline-groupset' in the event target's element
  6825. * tree, then finds the right group in this groupset
  6826. * @param {Event} event
  6827. * @return {Group | null} group
  6828. */
  6829. GroupSet.groupFromTarget = function groupFromTarget (event) {
  6830. var groupset,
  6831. target = event.target;
  6832. while (target) {
  6833. if (target.hasOwnProperty('timeline-groupset')) {
  6834. groupset = target['timeline-groupset'];
  6835. break;
  6836. }
  6837. target = target.parentNode;
  6838. }
  6839. if (groupset) {
  6840. for (var groupId in groupset.groups) {
  6841. if (groupset.groups.hasOwnProperty(groupId)) {
  6842. var group = groupset.groups[groupId];
  6843. if (group.itemset && ItemSet.itemSetFromTarget(event) == group.itemset) {
  6844. return group;
  6845. }
  6846. }
  6847. }
  6848. }
  6849. return null;
  6850. };
  6851. /**
  6852. * Create a timeline visualization
  6853. * @param {HTMLElement} container
  6854. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  6855. * @param {Object} [options] See Timeline.setOptions for the available options.
  6856. * @constructor
  6857. */
  6858. function Timeline (container, items, options) {
  6859. var me = this;
  6860. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6861. this.options = {
  6862. orientation: 'bottom',
  6863. autoResize: true,
  6864. editable: false,
  6865. selectable: true,
  6866. snap: null, // will be specified after timeaxis is created
  6867. min: null,
  6868. max: null,
  6869. zoomMin: 10, // milliseconds
  6870. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6871. // moveable: true, // TODO: option moveable
  6872. // zoomable: true, // TODO: option zoomable
  6873. showMinorLabels: true,
  6874. showMajorLabels: true,
  6875. showCurrentTime: false,
  6876. showCustomTime: false,
  6877. onAdd: function (item, callback) {
  6878. callback(item);
  6879. },
  6880. onUpdate: function (item, callback) {
  6881. callback(item);
  6882. },
  6883. onMove: function (item, callback) {
  6884. callback(item);
  6885. },
  6886. onRemove: function (item, callback) {
  6887. callback(item);
  6888. }
  6889. };
  6890. // controller
  6891. this.controller = new Controller();
  6892. // root panel
  6893. if (!container) {
  6894. throw new Error('No container element provided');
  6895. }
  6896. var rootOptions = Object.create(this.options);
  6897. rootOptions.height = function () {
  6898. // TODO: change to height
  6899. if (me.options.height) {
  6900. // fixed height
  6901. return me.options.height;
  6902. }
  6903. else {
  6904. // auto height
  6905. return (me.timeaxis.height + me.content.height) + 'px';
  6906. }
  6907. };
  6908. this.rootPanel = new RootPanel(container, rootOptions);
  6909. this.controller.add(this.rootPanel);
  6910. // single select (or unselect) when tapping an item
  6911. this.controller.on('tap', this._onSelectItem.bind(this));
  6912. // multi select when holding mouse/touch, or on ctrl+click
  6913. this.controller.on('hold', this._onMultiSelectItem.bind(this));
  6914. // add item on doubletap
  6915. this.controller.on('doubletap', this._onAddItem.bind(this));
  6916. // item panel
  6917. var itemOptions = Object.create(this.options);
  6918. itemOptions.left = function () {
  6919. return me.labelPanel.width;
  6920. };
  6921. itemOptions.width = function () {
  6922. return me.rootPanel.width - me.labelPanel.width;
  6923. };
  6924. itemOptions.top = null;
  6925. itemOptions.height = null;
  6926. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  6927. this.controller.add(this.itemPanel);
  6928. // label panel
  6929. var labelOptions = Object.create(this.options);
  6930. labelOptions.top = null;
  6931. labelOptions.left = null;
  6932. labelOptions.height = null;
  6933. labelOptions.width = function () {
  6934. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  6935. return me.content.getLabelsWidth();
  6936. }
  6937. else {
  6938. return 0;
  6939. }
  6940. };
  6941. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  6942. this.controller.add(this.labelPanel);
  6943. // range
  6944. var rangeOptions = Object.create(this.options);
  6945. this.range = new Range(rangeOptions);
  6946. this.range.setRange(
  6947. now.clone().add('days', -3).valueOf(),
  6948. now.clone().add('days', 4).valueOf()
  6949. );
  6950. this.range.subscribe(this.controller, this.rootPanel, 'move', 'horizontal');
  6951. this.range.subscribe(this.controller, this.rootPanel, 'zoom', 'horizontal');
  6952. this.range.on('rangechange', function (properties) {
  6953. var force = true;
  6954. me.controller.emit('rangechange', properties);
  6955. me.controller.emit('request-reflow', force);
  6956. });
  6957. this.range.on('rangechanged', function (properties) {
  6958. var force = true;
  6959. me.controller.emit('rangechanged', properties);
  6960. me.controller.emit('request-reflow', force);
  6961. });
  6962. // time axis
  6963. var timeaxisOptions = Object.create(rootOptions);
  6964. timeaxisOptions.range = this.range;
  6965. timeaxisOptions.left = null;
  6966. timeaxisOptions.top = null;
  6967. timeaxisOptions.width = '100%';
  6968. timeaxisOptions.height = null;
  6969. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  6970. this.timeaxis.setRange(this.range);
  6971. this.controller.add(this.timeaxis);
  6972. this.options.snap = this.timeaxis.snap.bind(this.timeaxis);
  6973. // current time bar
  6974. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  6975. this.controller.add(this.currenttime);
  6976. // custom time bar
  6977. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  6978. this.controller.add(this.customtime);
  6979. // create groupset
  6980. this.setGroups(null);
  6981. this.itemsData = null; // DataSet
  6982. this.groupsData = null; // DataSet
  6983. // apply options
  6984. if (options) {
  6985. this.setOptions(options);
  6986. }
  6987. // create itemset and groupset
  6988. if (items) {
  6989. this.setItems(items);
  6990. }
  6991. }
  6992. /**
  6993. * Add an event listener to the timeline
  6994. * @param {String} event Available events: select, rangechange, rangechanged,
  6995. * timechange, timechanged
  6996. * @param {function} callback
  6997. */
  6998. Timeline.prototype.on = function on (event, callback) {
  6999. this.controller.on(event, callback);
  7000. };
  7001. /**
  7002. * Add an event listener from the timeline
  7003. * @param {String} event
  7004. * @param {function} callback
  7005. */
  7006. Timeline.prototype.off = function off (event, callback) {
  7007. this.controller.off(event, callback);
  7008. };
  7009. /**
  7010. * Set options
  7011. * @param {Object} options TODO: describe the available options
  7012. */
  7013. Timeline.prototype.setOptions = function (options) {
  7014. util.extend(this.options, options);
  7015. // force update of range (apply new min/max etc.)
  7016. // both start and end are optional
  7017. this.range.setRange(options.start, options.end);
  7018. if ('editable' in options || 'selectable' in options) {
  7019. if (this.options.selectable) {
  7020. // force update of selection
  7021. this.setSelection(this.getSelection());
  7022. }
  7023. else {
  7024. // remove selection
  7025. this.setSelection([]);
  7026. }
  7027. }
  7028. // validate the callback functions
  7029. var validateCallback = (function (fn) {
  7030. if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
  7031. throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
  7032. }
  7033. }).bind(this);
  7034. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
  7035. this.controller.reflow();
  7036. this.controller.repaint();
  7037. };
  7038. /**
  7039. * Set a custom time bar
  7040. * @param {Date} time
  7041. */
  7042. Timeline.prototype.setCustomTime = function (time) {
  7043. if (!this.customtime) {
  7044. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  7045. }
  7046. this.customtime.setCustomTime(time);
  7047. };
  7048. /**
  7049. * Retrieve the current custom time.
  7050. * @return {Date} customTime
  7051. */
  7052. Timeline.prototype.getCustomTime = function() {
  7053. if (!this.customtime) {
  7054. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  7055. }
  7056. return this.customtime.getCustomTime();
  7057. };
  7058. /**
  7059. * Set items
  7060. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  7061. */
  7062. Timeline.prototype.setItems = function(items) {
  7063. var initialLoad = (this.itemsData == null);
  7064. // convert to type DataSet when needed
  7065. var newDataSet;
  7066. if (!items) {
  7067. newDataSet = null;
  7068. }
  7069. else if (items instanceof DataSet) {
  7070. newDataSet = items;
  7071. }
  7072. if (!(items instanceof DataSet)) {
  7073. newDataSet = new DataSet({
  7074. convert: {
  7075. start: 'Date',
  7076. end: 'Date'
  7077. }
  7078. });
  7079. newDataSet.add(items);
  7080. }
  7081. // set items
  7082. this.itemsData = newDataSet;
  7083. this.content.setItems(newDataSet);
  7084. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  7085. // apply the data range as range
  7086. var dataRange = this.getItemRange();
  7087. // add 5% space on both sides
  7088. var start = dataRange.min;
  7089. var end = dataRange.max;
  7090. if (start != null && end != null) {
  7091. var interval = (end.valueOf() - start.valueOf());
  7092. if (interval <= 0) {
  7093. // prevent an empty interval
  7094. interval = 24 * 60 * 60 * 1000; // 1 day
  7095. }
  7096. start = new Date(start.valueOf() - interval * 0.05);
  7097. end = new Date(end.valueOf() + interval * 0.05);
  7098. }
  7099. // override specified start and/or end date
  7100. if (this.options.start != undefined) {
  7101. start = util.convert(this.options.start, 'Date');
  7102. }
  7103. if (this.options.end != undefined) {
  7104. end = util.convert(this.options.end, 'Date');
  7105. }
  7106. // apply range if there is a min or max available
  7107. if (start != null || end != null) {
  7108. this.range.setRange(start, end);
  7109. }
  7110. }
  7111. };
  7112. /**
  7113. * Set groups
  7114. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  7115. */
  7116. Timeline.prototype.setGroups = function(groups) {
  7117. var me = this;
  7118. this.groupsData = groups;
  7119. // switch content type between ItemSet or GroupSet when needed
  7120. var Type = this.groupsData ? GroupSet : ItemSet;
  7121. if (!(this.content instanceof Type)) {
  7122. // remove old content set
  7123. if (this.content) {
  7124. this.content.hide();
  7125. if (this.content.setItems) {
  7126. this.content.setItems(); // disconnect from items
  7127. }
  7128. if (this.content.setGroups) {
  7129. this.content.setGroups(); // disconnect from groups
  7130. }
  7131. this.controller.remove(this.content);
  7132. }
  7133. // create new content set
  7134. var options = Object.create(this.options);
  7135. util.extend(options, {
  7136. top: function () {
  7137. if (me.options.orientation == 'top') {
  7138. return me.timeaxis.height;
  7139. }
  7140. else {
  7141. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  7142. }
  7143. },
  7144. left: null,
  7145. width: '100%',
  7146. height: function () {
  7147. if (me.options.height) {
  7148. // fixed height
  7149. return me.itemPanel.height - me.timeaxis.height;
  7150. }
  7151. else {
  7152. // auto height
  7153. return null;
  7154. }
  7155. },
  7156. maxHeight: function () {
  7157. // TODO: change maxHeight to be a css string like '100%' or '300px'
  7158. if (me.options.maxHeight) {
  7159. if (!util.isNumber(me.options.maxHeight)) {
  7160. throw new TypeError('Number expected for property maxHeight');
  7161. }
  7162. return me.options.maxHeight - me.timeaxis.height;
  7163. }
  7164. else {
  7165. return null;
  7166. }
  7167. },
  7168. labelContainer: function () {
  7169. return me.labelPanel.getContainer();
  7170. }
  7171. });
  7172. this.content = new Type(this.itemPanel, [this.timeaxis], options);
  7173. if (this.content.setRange) {
  7174. this.content.setRange(this.range);
  7175. }
  7176. if (this.content.setItems) {
  7177. this.content.setItems(this.itemsData);
  7178. }
  7179. if (this.content.setGroups) {
  7180. this.content.setGroups(this.groupsData);
  7181. }
  7182. this.controller.add(this.content);
  7183. }
  7184. };
  7185. /**
  7186. * Get the data range of the item set.
  7187. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  7188. * When no minimum is found, min==null
  7189. * When no maximum is found, max==null
  7190. */
  7191. Timeline.prototype.getItemRange = function getItemRange() {
  7192. // calculate min from start filed
  7193. var itemsData = this.itemsData,
  7194. min = null,
  7195. max = null;
  7196. if (itemsData) {
  7197. // calculate the minimum value of the field 'start'
  7198. var minItem = itemsData.min('start');
  7199. min = minItem ? minItem.start.valueOf() : null;
  7200. // calculate maximum value of fields 'start' and 'end'
  7201. var maxStartItem = itemsData.max('start');
  7202. if (maxStartItem) {
  7203. max = maxStartItem.start.valueOf();
  7204. }
  7205. var maxEndItem = itemsData.max('end');
  7206. if (maxEndItem) {
  7207. if (max == null) {
  7208. max = maxEndItem.end.valueOf();
  7209. }
  7210. else {
  7211. max = Math.max(max, maxEndItem.end.valueOf());
  7212. }
  7213. }
  7214. }
  7215. return {
  7216. min: (min != null) ? new Date(min) : null,
  7217. max: (max != null) ? new Date(max) : null
  7218. };
  7219. };
  7220. /**
  7221. * Set selected items by their id. Replaces the current selection
  7222. * Unknown id's are silently ignored.
  7223. * @param {Array} [ids] An array with zero or more id's of the items to be
  7224. * selected. If ids is an empty array, all items will be
  7225. * unselected.
  7226. */
  7227. Timeline.prototype.setSelection = function setSelection (ids) {
  7228. if (this.content) this.content.setSelection(ids);
  7229. };
  7230. /**
  7231. * Get the selected items by their id
  7232. * @return {Array} ids The ids of the selected items
  7233. */
  7234. Timeline.prototype.getSelection = function getSelection() {
  7235. return this.content ? this.content.getSelection() : [];
  7236. };
  7237. /**
  7238. * Set the visible window. Both parameters are optional, you can change only
  7239. * start or only end.
  7240. * @param {Date | Number | String} [start] Start date of visible window
  7241. * @param {Date | Number | String} [end] End date of visible window
  7242. */
  7243. Timeline.prototype.setWindow = function setWindow(start, end) {
  7244. this.range.setRange(start, end);
  7245. };
  7246. /**
  7247. * Get the visible window
  7248. * @return {{start: Date, end: Date}} Visible range
  7249. */
  7250. Timeline.prototype.getWindow = function setWindow() {
  7251. var range = this.range.getRange();
  7252. return {
  7253. start: new Date(range.start),
  7254. end: new Date(range.end)
  7255. };
  7256. };
  7257. /**
  7258. * Handle selecting/deselecting an item when tapping it
  7259. * @param {Event} event
  7260. * @private
  7261. */
  7262. // TODO: move this function to ItemSet
  7263. Timeline.prototype._onSelectItem = function (event) {
  7264. if (!this.options.selectable) return;
  7265. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  7266. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  7267. if (ctrlKey || shiftKey) {
  7268. this._onMultiSelectItem(event);
  7269. return;
  7270. }
  7271. var item = ItemSet.itemFromTarget(event);
  7272. var selection = item ? [item.id] : [];
  7273. this.setSelection(selection);
  7274. this.controller.emit('select', {
  7275. items: this.getSelection()
  7276. });
  7277. event.stopPropagation();
  7278. };
  7279. /**
  7280. * Handle creation and updates of an item on double tap
  7281. * @param event
  7282. * @private
  7283. */
  7284. Timeline.prototype._onAddItem = function (event) {
  7285. if (!this.options.selectable) return;
  7286. if (!this.options.editable) return;
  7287. var me = this,
  7288. item = ItemSet.itemFromTarget(event);
  7289. if (item) {
  7290. // update item
  7291. // execute async handler to update the item (or cancel it)
  7292. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  7293. this.options.onUpdate(itemData, function (itemData) {
  7294. if (itemData) {
  7295. me.itemsData.update(itemData);
  7296. }
  7297. });
  7298. }
  7299. else {
  7300. // add item
  7301. var xAbs = vis.util.getAbsoluteLeft(this.rootPanel.frame);
  7302. var x = event.gesture.center.pageX - xAbs;
  7303. var newItem = {
  7304. start: this.timeaxis.snap(this._toTime(x)),
  7305. content: 'new item'
  7306. };
  7307. var id = util.randomUUID();
  7308. newItem[this.itemsData.fieldId] = id;
  7309. var group = GroupSet.groupFromTarget(event);
  7310. if (group) {
  7311. newItem.group = group.groupId;
  7312. }
  7313. // execute async handler to customize (or cancel) adding an item
  7314. this.options.onAdd(newItem, function (item) {
  7315. if (item) {
  7316. me.itemsData.add(newItem);
  7317. // select the created item after it is repainted
  7318. me.controller.once('repaint', function () {
  7319. me.setSelection([id]);
  7320. me.controller.emit('select', {
  7321. items: me.getSelection()
  7322. });
  7323. }.bind(me));
  7324. }
  7325. });
  7326. }
  7327. };
  7328. /**
  7329. * Handle selecting/deselecting multiple items when holding an item
  7330. * @param {Event} event
  7331. * @private
  7332. */
  7333. // TODO: move this function to ItemSet
  7334. Timeline.prototype._onMultiSelectItem = function (event) {
  7335. if (!this.options.selectable) return;
  7336. var selection,
  7337. item = ItemSet.itemFromTarget(event);
  7338. if (item) {
  7339. // multi select items
  7340. selection = this.getSelection(); // current selection
  7341. var index = selection.indexOf(item.id);
  7342. if (index == -1) {
  7343. // item is not yet selected -> select it
  7344. selection.push(item.id);
  7345. }
  7346. else {
  7347. // item is already selected -> deselect it
  7348. selection.splice(index, 1);
  7349. }
  7350. this.setSelection(selection);
  7351. this.controller.emit('select', {
  7352. items: this.getSelection()
  7353. });
  7354. event.stopPropagation();
  7355. }
  7356. };
  7357. /**
  7358. * Convert a position on screen (pixels) to a datetime
  7359. * @param {int} x Position on the screen in pixels
  7360. * @return {Date} time The datetime the corresponds with given position x
  7361. * @private
  7362. */
  7363. Timeline.prototype._toTime = function _toTime(x) {
  7364. var conversion = this.range.conversion(this.content.width);
  7365. return new Date(x / conversion.scale + conversion.offset);
  7366. };
  7367. /**
  7368. * Convert a datetime (Date object) into a position on the screen
  7369. * @param {Date} time A date
  7370. * @return {int} x The position on the screen in pixels which corresponds
  7371. * with the given date.
  7372. * @private
  7373. */
  7374. Timeline.prototype._toScreen = function _toScreen(time) {
  7375. var conversion = this.range.conversion(this.content.width);
  7376. return (time.valueOf() - conversion.offset) * conversion.scale;
  7377. };
  7378. (function(exports) {
  7379. /**
  7380. * Parse a text source containing data in DOT language into a JSON object.
  7381. * The object contains two lists: one with nodes and one with edges.
  7382. *
  7383. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  7384. *
  7385. * @param {String} data Text containing a graph in DOT-notation
  7386. * @return {Object} graph An object containing two parameters:
  7387. * {Object[]} nodes
  7388. * {Object[]} edges
  7389. */
  7390. function parseDOT (data) {
  7391. dot = data;
  7392. return parseGraph();
  7393. }
  7394. // token types enumeration
  7395. var TOKENTYPE = {
  7396. NULL : 0,
  7397. DELIMITER : 1,
  7398. IDENTIFIER: 2,
  7399. UNKNOWN : 3
  7400. };
  7401. // map with all delimiters
  7402. var DELIMITERS = {
  7403. '{': true,
  7404. '}': true,
  7405. '[': true,
  7406. ']': true,
  7407. ';': true,
  7408. '=': true,
  7409. ',': true,
  7410. '->': true,
  7411. '--': true
  7412. };
  7413. var dot = ''; // current dot file
  7414. var index = 0; // current index in dot file
  7415. var c = ''; // current token character in expr
  7416. var token = ''; // current token
  7417. var tokenType = TOKENTYPE.NULL; // type of the token
  7418. /**
  7419. * Get the first character from the dot file.
  7420. * The character is stored into the char c. If the end of the dot file is
  7421. * reached, the function puts an empty string in c.
  7422. */
  7423. function first() {
  7424. index = 0;
  7425. c = dot.charAt(0);
  7426. }
  7427. /**
  7428. * Get the next character from the dot file.
  7429. * The character is stored into the char c. If the end of the dot file is
  7430. * reached, the function puts an empty string in c.
  7431. */
  7432. function next() {
  7433. index++;
  7434. c = dot.charAt(index);
  7435. }
  7436. /**
  7437. * Preview the next character from the dot file.
  7438. * @return {String} cNext
  7439. */
  7440. function nextPreview() {
  7441. return dot.charAt(index + 1);
  7442. }
  7443. /**
  7444. * Test whether given character is alphabetic or numeric
  7445. * @param {String} c
  7446. * @return {Boolean} isAlphaNumeric
  7447. */
  7448. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  7449. function isAlphaNumeric(c) {
  7450. return regexAlphaNumeric.test(c);
  7451. }
  7452. /**
  7453. * Merge all properties of object b into object b
  7454. * @param {Object} a
  7455. * @param {Object} b
  7456. * @return {Object} a
  7457. */
  7458. function merge (a, b) {
  7459. if (!a) {
  7460. a = {};
  7461. }
  7462. if (b) {
  7463. for (var name in b) {
  7464. if (b.hasOwnProperty(name)) {
  7465. a[name] = b[name];
  7466. }
  7467. }
  7468. }
  7469. return a;
  7470. }
  7471. /**
  7472. * Set a value in an object, where the provided parameter name can be a
  7473. * path with nested parameters. For example:
  7474. *
  7475. * var obj = {a: 2};
  7476. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  7477. *
  7478. * @param {Object} obj
  7479. * @param {String} path A parameter name or dot-separated parameter path,
  7480. * like "color.highlight.border".
  7481. * @param {*} value
  7482. */
  7483. function setValue(obj, path, value) {
  7484. var keys = path.split('.');
  7485. var o = obj;
  7486. while (keys.length) {
  7487. var key = keys.shift();
  7488. if (keys.length) {
  7489. // this isn't the end point
  7490. if (!o[key]) {
  7491. o[key] = {};
  7492. }
  7493. o = o[key];
  7494. }
  7495. else {
  7496. // this is the end point
  7497. o[key] = value;
  7498. }
  7499. }
  7500. }
  7501. /**
  7502. * Add a node to a graph object. If there is already a node with
  7503. * the same id, their attributes will be merged.
  7504. * @param {Object} graph
  7505. * @param {Object} node
  7506. */
  7507. function addNode(graph, node) {
  7508. var i, len;
  7509. var current = null;
  7510. // find root graph (in case of subgraph)
  7511. var graphs = [graph]; // list with all graphs from current graph to root graph
  7512. var root = graph;
  7513. while (root.parent) {
  7514. graphs.push(root.parent);
  7515. root = root.parent;
  7516. }
  7517. // find existing node (at root level) by its id
  7518. if (root.nodes) {
  7519. for (i = 0, len = root.nodes.length; i < len; i++) {
  7520. if (node.id === root.nodes[i].id) {
  7521. current = root.nodes[i];
  7522. break;
  7523. }
  7524. }
  7525. }
  7526. if (!current) {
  7527. // this is a new node
  7528. current = {
  7529. id: node.id
  7530. };
  7531. if (graph.node) {
  7532. // clone default attributes
  7533. current.attr = merge(current.attr, graph.node);
  7534. }
  7535. }
  7536. // add node to this (sub)graph and all its parent graphs
  7537. for (i = graphs.length - 1; i >= 0; i--) {
  7538. var g = graphs[i];
  7539. if (!g.nodes) {
  7540. g.nodes = [];
  7541. }
  7542. if (g.nodes.indexOf(current) == -1) {
  7543. g.nodes.push(current);
  7544. }
  7545. }
  7546. // merge attributes
  7547. if (node.attr) {
  7548. current.attr = merge(current.attr, node.attr);
  7549. }
  7550. }
  7551. /**
  7552. * Add an edge to a graph object
  7553. * @param {Object} graph
  7554. * @param {Object} edge
  7555. */
  7556. function addEdge(graph, edge) {
  7557. if (!graph.edges) {
  7558. graph.edges = [];
  7559. }
  7560. graph.edges.push(edge);
  7561. if (graph.edge) {
  7562. var attr = merge({}, graph.edge); // clone default attributes
  7563. edge.attr = merge(attr, edge.attr); // merge attributes
  7564. }
  7565. }
  7566. /**
  7567. * Create an edge to a graph object
  7568. * @param {Object} graph
  7569. * @param {String | Number | Object} from
  7570. * @param {String | Number | Object} to
  7571. * @param {String} type
  7572. * @param {Object | null} attr
  7573. * @return {Object} edge
  7574. */
  7575. function createEdge(graph, from, to, type, attr) {
  7576. var edge = {
  7577. from: from,
  7578. to: to,
  7579. type: type
  7580. };
  7581. if (graph.edge) {
  7582. edge.attr = merge({}, graph.edge); // clone default attributes
  7583. }
  7584. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  7585. return edge;
  7586. }
  7587. /**
  7588. * Get next token in the current dot file.
  7589. * The token and token type are available as token and tokenType
  7590. */
  7591. function getToken() {
  7592. tokenType = TOKENTYPE.NULL;
  7593. token = '';
  7594. // skip over whitespaces
  7595. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7596. next();
  7597. }
  7598. do {
  7599. var isComment = false;
  7600. // skip comment
  7601. if (c == '#') {
  7602. // find the previous non-space character
  7603. var i = index - 1;
  7604. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7605. i--;
  7606. }
  7607. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7608. // the # is at the start of a line, this is indeed a line comment
  7609. while (c != '' && c != '\n') {
  7610. next();
  7611. }
  7612. isComment = true;
  7613. }
  7614. }
  7615. if (c == '/' && nextPreview() == '/') {
  7616. // skip line comment
  7617. while (c != '' && c != '\n') {
  7618. next();
  7619. }
  7620. isComment = true;
  7621. }
  7622. if (c == '/' && nextPreview() == '*') {
  7623. // skip block comment
  7624. while (c != '') {
  7625. if (c == '*' && nextPreview() == '/') {
  7626. // end of block comment found. skip these last two characters
  7627. next();
  7628. next();
  7629. break;
  7630. }
  7631. else {
  7632. next();
  7633. }
  7634. }
  7635. isComment = true;
  7636. }
  7637. // skip over whitespaces
  7638. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7639. next();
  7640. }
  7641. }
  7642. while (isComment);
  7643. // check for end of dot file
  7644. if (c == '') {
  7645. // token is still empty
  7646. tokenType = TOKENTYPE.DELIMITER;
  7647. return;
  7648. }
  7649. // check for delimiters consisting of 2 characters
  7650. var c2 = c + nextPreview();
  7651. if (DELIMITERS[c2]) {
  7652. tokenType = TOKENTYPE.DELIMITER;
  7653. token = c2;
  7654. next();
  7655. next();
  7656. return;
  7657. }
  7658. // check for delimiters consisting of 1 character
  7659. if (DELIMITERS[c]) {
  7660. tokenType = TOKENTYPE.DELIMITER;
  7661. token = c;
  7662. next();
  7663. return;
  7664. }
  7665. // check for an identifier (number or string)
  7666. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7667. if (isAlphaNumeric(c) || c == '-') {
  7668. token += c;
  7669. next();
  7670. while (isAlphaNumeric(c)) {
  7671. token += c;
  7672. next();
  7673. }
  7674. if (token == 'false') {
  7675. token = false; // convert to boolean
  7676. }
  7677. else if (token == 'true') {
  7678. token = true; // convert to boolean
  7679. }
  7680. else if (!isNaN(Number(token))) {
  7681. token = Number(token); // convert to number
  7682. }
  7683. tokenType = TOKENTYPE.IDENTIFIER;
  7684. return;
  7685. }
  7686. // check for a string enclosed by double quotes
  7687. if (c == '"') {
  7688. next();
  7689. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7690. token += c;
  7691. if (c == '"') { // skip the escape character
  7692. next();
  7693. }
  7694. next();
  7695. }
  7696. if (c != '"') {
  7697. throw newSyntaxError('End of string " expected');
  7698. }
  7699. next();
  7700. tokenType = TOKENTYPE.IDENTIFIER;
  7701. return;
  7702. }
  7703. // something unknown is found, wrong characters, a syntax error
  7704. tokenType = TOKENTYPE.UNKNOWN;
  7705. while (c != '') {
  7706. token += c;
  7707. next();
  7708. }
  7709. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7710. }
  7711. /**
  7712. * Parse a graph.
  7713. * @returns {Object} graph
  7714. */
  7715. function parseGraph() {
  7716. var graph = {};
  7717. first();
  7718. getToken();
  7719. // optional strict keyword
  7720. if (token == 'strict') {
  7721. graph.strict = true;
  7722. getToken();
  7723. }
  7724. // graph or digraph keyword
  7725. if (token == 'graph' || token == 'digraph') {
  7726. graph.type = token;
  7727. getToken();
  7728. }
  7729. // optional graph id
  7730. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7731. graph.id = token;
  7732. getToken();
  7733. }
  7734. // open angle bracket
  7735. if (token != '{') {
  7736. throw newSyntaxError('Angle bracket { expected');
  7737. }
  7738. getToken();
  7739. // statements
  7740. parseStatements(graph);
  7741. // close angle bracket
  7742. if (token != '}') {
  7743. throw newSyntaxError('Angle bracket } expected');
  7744. }
  7745. getToken();
  7746. // end of file
  7747. if (token !== '') {
  7748. throw newSyntaxError('End of file expected');
  7749. }
  7750. getToken();
  7751. // remove temporary default properties
  7752. delete graph.node;
  7753. delete graph.edge;
  7754. delete graph.graph;
  7755. return graph;
  7756. }
  7757. /**
  7758. * Parse a list with statements.
  7759. * @param {Object} graph
  7760. */
  7761. function parseStatements (graph) {
  7762. while (token !== '' && token != '}') {
  7763. parseStatement(graph);
  7764. if (token == ';') {
  7765. getToken();
  7766. }
  7767. }
  7768. }
  7769. /**
  7770. * Parse a single statement. Can be a an attribute statement, node
  7771. * statement, a series of node statements and edge statements, or a
  7772. * parameter.
  7773. * @param {Object} graph
  7774. */
  7775. function parseStatement(graph) {
  7776. // parse subgraph
  7777. var subgraph = parseSubgraph(graph);
  7778. if (subgraph) {
  7779. // edge statements
  7780. parseEdge(graph, subgraph);
  7781. return;
  7782. }
  7783. // parse an attribute statement
  7784. var attr = parseAttributeStatement(graph);
  7785. if (attr) {
  7786. return;
  7787. }
  7788. // parse node
  7789. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7790. throw newSyntaxError('Identifier expected');
  7791. }
  7792. var id = token; // id can be a string or a number
  7793. getToken();
  7794. if (token == '=') {
  7795. // id statement
  7796. getToken();
  7797. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7798. throw newSyntaxError('Identifier expected');
  7799. }
  7800. graph[id] = token;
  7801. getToken();
  7802. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7803. }
  7804. else {
  7805. parseNodeStatement(graph, id);
  7806. }
  7807. }
  7808. /**
  7809. * Parse a subgraph
  7810. * @param {Object} graph parent graph object
  7811. * @return {Object | null} subgraph
  7812. */
  7813. function parseSubgraph (graph) {
  7814. var subgraph = null;
  7815. // optional subgraph keyword
  7816. if (token == 'subgraph') {
  7817. subgraph = {};
  7818. subgraph.type = 'subgraph';
  7819. getToken();
  7820. // optional graph id
  7821. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7822. subgraph.id = token;
  7823. getToken();
  7824. }
  7825. }
  7826. // open angle bracket
  7827. if (token == '{') {
  7828. getToken();
  7829. if (!subgraph) {
  7830. subgraph = {};
  7831. }
  7832. subgraph.parent = graph;
  7833. subgraph.node = graph.node;
  7834. subgraph.edge = graph.edge;
  7835. subgraph.graph = graph.graph;
  7836. // statements
  7837. parseStatements(subgraph);
  7838. // close angle bracket
  7839. if (token != '}') {
  7840. throw newSyntaxError('Angle bracket } expected');
  7841. }
  7842. getToken();
  7843. // remove temporary default properties
  7844. delete subgraph.node;
  7845. delete subgraph.edge;
  7846. delete subgraph.graph;
  7847. delete subgraph.parent;
  7848. // register at the parent graph
  7849. if (!graph.subgraphs) {
  7850. graph.subgraphs = [];
  7851. }
  7852. graph.subgraphs.push(subgraph);
  7853. }
  7854. return subgraph;
  7855. }
  7856. /**
  7857. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7858. * Available keywords are 'node', 'edge', 'graph'.
  7859. * The previous list with default attributes will be replaced
  7860. * @param {Object} graph
  7861. * @returns {String | null} keyword Returns the name of the parsed attribute
  7862. * (node, edge, graph), or null if nothing
  7863. * is parsed.
  7864. */
  7865. function parseAttributeStatement (graph) {
  7866. // attribute statements
  7867. if (token == 'node') {
  7868. getToken();
  7869. // node attributes
  7870. graph.node = parseAttributeList();
  7871. return 'node';
  7872. }
  7873. else if (token == 'edge') {
  7874. getToken();
  7875. // edge attributes
  7876. graph.edge = parseAttributeList();
  7877. return 'edge';
  7878. }
  7879. else if (token == 'graph') {
  7880. getToken();
  7881. // graph attributes
  7882. graph.graph = parseAttributeList();
  7883. return 'graph';
  7884. }
  7885. return null;
  7886. }
  7887. /**
  7888. * parse a node statement
  7889. * @param {Object} graph
  7890. * @param {String | Number} id
  7891. */
  7892. function parseNodeStatement(graph, id) {
  7893. // node statement
  7894. var node = {
  7895. id: id
  7896. };
  7897. var attr = parseAttributeList();
  7898. if (attr) {
  7899. node.attr = attr;
  7900. }
  7901. addNode(graph, node);
  7902. // edge statements
  7903. parseEdge(graph, id);
  7904. }
  7905. /**
  7906. * Parse an edge or a series of edges
  7907. * @param {Object} graph
  7908. * @param {String | Number} from Id of the from node
  7909. */
  7910. function parseEdge(graph, from) {
  7911. while (token == '->' || token == '--') {
  7912. var to;
  7913. var type = token;
  7914. getToken();
  7915. var subgraph = parseSubgraph(graph);
  7916. if (subgraph) {
  7917. to = subgraph;
  7918. }
  7919. else {
  7920. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7921. throw newSyntaxError('Identifier or subgraph expected');
  7922. }
  7923. to = token;
  7924. addNode(graph, {
  7925. id: to
  7926. });
  7927. getToken();
  7928. }
  7929. // parse edge attributes
  7930. var attr = parseAttributeList();
  7931. // create edge
  7932. var edge = createEdge(graph, from, to, type, attr);
  7933. addEdge(graph, edge);
  7934. from = to;
  7935. }
  7936. }
  7937. /**
  7938. * Parse a set with attributes,
  7939. * for example [label="1.000", shape=solid]
  7940. * @return {Object | null} attr
  7941. */
  7942. function parseAttributeList() {
  7943. var attr = null;
  7944. while (token == '[') {
  7945. getToken();
  7946. attr = {};
  7947. while (token !== '' && token != ']') {
  7948. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7949. throw newSyntaxError('Attribute name expected');
  7950. }
  7951. var name = token;
  7952. getToken();
  7953. if (token != '=') {
  7954. throw newSyntaxError('Equal sign = expected');
  7955. }
  7956. getToken();
  7957. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7958. throw newSyntaxError('Attribute value expected');
  7959. }
  7960. var value = token;
  7961. setValue(attr, name, value); // name can be a path
  7962. getToken();
  7963. if (token ==',') {
  7964. getToken();
  7965. }
  7966. }
  7967. if (token != ']') {
  7968. throw newSyntaxError('Bracket ] expected');
  7969. }
  7970. getToken();
  7971. }
  7972. return attr;
  7973. }
  7974. /**
  7975. * Create a syntax error with extra information on current token and index.
  7976. * @param {String} message
  7977. * @returns {SyntaxError} err
  7978. */
  7979. function newSyntaxError(message) {
  7980. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7981. }
  7982. /**
  7983. * Chop off text after a maximum length
  7984. * @param {String} text
  7985. * @param {Number} maxLength
  7986. * @returns {String}
  7987. */
  7988. function chop (text, maxLength) {
  7989. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7990. }
  7991. /**
  7992. * Execute a function fn for each pair of elements in two arrays
  7993. * @param {Array | *} array1
  7994. * @param {Array | *} array2
  7995. * @param {function} fn
  7996. */
  7997. function forEach2(array1, array2, fn) {
  7998. if (array1 instanceof Array) {
  7999. array1.forEach(function (elem1) {
  8000. if (array2 instanceof Array) {
  8001. array2.forEach(function (elem2) {
  8002. fn(elem1, elem2);
  8003. });
  8004. }
  8005. else {
  8006. fn(elem1, array2);
  8007. }
  8008. });
  8009. }
  8010. else {
  8011. if (array2 instanceof Array) {
  8012. array2.forEach(function (elem2) {
  8013. fn(array1, elem2);
  8014. });
  8015. }
  8016. else {
  8017. fn(array1, array2);
  8018. }
  8019. }
  8020. }
  8021. /**
  8022. * Convert a string containing a graph in DOT language into a map containing
  8023. * with nodes and edges in the format of graph.
  8024. * @param {String} data Text containing a graph in DOT-notation
  8025. * @return {Object} graphData
  8026. */
  8027. function DOTToGraph (data) {
  8028. // parse the DOT file
  8029. var dotData = parseDOT(data);
  8030. var graphData = {
  8031. nodes: [],
  8032. edges: [],
  8033. options: {}
  8034. };
  8035. // copy the nodes
  8036. if (dotData.nodes) {
  8037. dotData.nodes.forEach(function (dotNode) {
  8038. var graphNode = {
  8039. id: dotNode.id,
  8040. label: String(dotNode.label || dotNode.id)
  8041. };
  8042. merge(graphNode, dotNode.attr);
  8043. if (graphNode.image) {
  8044. graphNode.shape = 'image';
  8045. }
  8046. graphData.nodes.push(graphNode);
  8047. });
  8048. }
  8049. // copy the edges
  8050. if (dotData.edges) {
  8051. /**
  8052. * Convert an edge in DOT format to an edge with VisGraph format
  8053. * @param {Object} dotEdge
  8054. * @returns {Object} graphEdge
  8055. */
  8056. function convertEdge(dotEdge) {
  8057. var graphEdge = {
  8058. from: dotEdge.from,
  8059. to: dotEdge.to
  8060. };
  8061. merge(graphEdge, dotEdge.attr);
  8062. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  8063. return graphEdge;
  8064. }
  8065. dotData.edges.forEach(function (dotEdge) {
  8066. var from, to;
  8067. if (dotEdge.from instanceof Object) {
  8068. from = dotEdge.from.nodes;
  8069. }
  8070. else {
  8071. from = {
  8072. id: dotEdge.from
  8073. }
  8074. }
  8075. if (dotEdge.to instanceof Object) {
  8076. to = dotEdge.to.nodes;
  8077. }
  8078. else {
  8079. to = {
  8080. id: dotEdge.to
  8081. }
  8082. }
  8083. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  8084. dotEdge.from.edges.forEach(function (subEdge) {
  8085. var graphEdge = convertEdge(subEdge);
  8086. graphData.edges.push(graphEdge);
  8087. });
  8088. }
  8089. forEach2(from, to, function (from, to) {
  8090. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  8091. var graphEdge = convertEdge(subEdge);
  8092. graphData.edges.push(graphEdge);
  8093. });
  8094. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  8095. dotEdge.to.edges.forEach(function (subEdge) {
  8096. var graphEdge = convertEdge(subEdge);
  8097. graphData.edges.push(graphEdge);
  8098. });
  8099. }
  8100. });
  8101. }
  8102. // copy the options
  8103. if (dotData.attr) {
  8104. graphData.options = dotData.attr;
  8105. }
  8106. return graphData;
  8107. }
  8108. // exports
  8109. exports.parseDOT = parseDOT;
  8110. exports.DOTToGraph = DOTToGraph;
  8111. })(typeof util !== 'undefined' ? util : exports);
  8112. /**
  8113. * Canvas shapes used by the Graph
  8114. */
  8115. if (typeof CanvasRenderingContext2D !== 'undefined') {
  8116. /**
  8117. * Draw a circle shape
  8118. */
  8119. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  8120. this.beginPath();
  8121. this.arc(x, y, r, 0, 2*Math.PI, false);
  8122. };
  8123. /**
  8124. * Draw a square shape
  8125. * @param {Number} x horizontal center
  8126. * @param {Number} y vertical center
  8127. * @param {Number} r size, width and height of the square
  8128. */
  8129. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  8130. this.beginPath();
  8131. this.rect(x - r, y - r, r * 2, r * 2);
  8132. };
  8133. /**
  8134. * Draw a triangle shape
  8135. * @param {Number} x horizontal center
  8136. * @param {Number} y vertical center
  8137. * @param {Number} r radius, half the length of the sides of the triangle
  8138. */
  8139. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  8140. // http://en.wikipedia.org/wiki/Equilateral_triangle
  8141. this.beginPath();
  8142. var s = r * 2;
  8143. var s2 = s / 2;
  8144. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  8145. var h = Math.sqrt(s * s - s2 * s2); // height
  8146. this.moveTo(x, y - (h - ir));
  8147. this.lineTo(x + s2, y + ir);
  8148. this.lineTo(x - s2, y + ir);
  8149. this.lineTo(x, y - (h - ir));
  8150. this.closePath();
  8151. };
  8152. /**
  8153. * Draw a triangle shape in downward orientation
  8154. * @param {Number} x horizontal center
  8155. * @param {Number} y vertical center
  8156. * @param {Number} r radius
  8157. */
  8158. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  8159. // http://en.wikipedia.org/wiki/Equilateral_triangle
  8160. this.beginPath();
  8161. var s = r * 2;
  8162. var s2 = s / 2;
  8163. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  8164. var h = Math.sqrt(s * s - s2 * s2); // height
  8165. this.moveTo(x, y + (h - ir));
  8166. this.lineTo(x + s2, y - ir);
  8167. this.lineTo(x - s2, y - ir);
  8168. this.lineTo(x, y + (h - ir));
  8169. this.closePath();
  8170. };
  8171. /**
  8172. * Draw a star shape, a star with 5 points
  8173. * @param {Number} x horizontal center
  8174. * @param {Number} y vertical center
  8175. * @param {Number} r radius, half the length of the sides of the triangle
  8176. */
  8177. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  8178. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  8179. this.beginPath();
  8180. for (var n = 0; n < 10; n++) {
  8181. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  8182. this.lineTo(
  8183. x + radius * Math.sin(n * 2 * Math.PI / 10),
  8184. y - radius * Math.cos(n * 2 * Math.PI / 10)
  8185. );
  8186. }
  8187. this.closePath();
  8188. };
  8189. /**
  8190. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  8191. */
  8192. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  8193. var r2d = Math.PI/180;
  8194. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  8195. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  8196. this.beginPath();
  8197. this.moveTo(x+r,y);
  8198. this.lineTo(x+w-r,y);
  8199. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  8200. this.lineTo(x+w,y+h-r);
  8201. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  8202. this.lineTo(x+r,y+h);
  8203. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  8204. this.lineTo(x,y+r);
  8205. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  8206. };
  8207. /**
  8208. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  8209. */
  8210. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  8211. var kappa = .5522848,
  8212. ox = (w / 2) * kappa, // control point offset horizontal
  8213. oy = (h / 2) * kappa, // control point offset vertical
  8214. xe = x + w, // x-end
  8215. ye = y + h, // y-end
  8216. xm = x + w / 2, // x-middle
  8217. ym = y + h / 2; // y-middle
  8218. this.beginPath();
  8219. this.moveTo(x, ym);
  8220. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  8221. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  8222. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  8223. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  8224. };
  8225. /**
  8226. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  8227. */
  8228. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  8229. var f = 1/3;
  8230. var wEllipse = w;
  8231. var hEllipse = h * f;
  8232. var kappa = .5522848,
  8233. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  8234. oy = (hEllipse / 2) * kappa, // control point offset vertical
  8235. xe = x + wEllipse, // x-end
  8236. ye = y + hEllipse, // y-end
  8237. xm = x + wEllipse / 2, // x-middle
  8238. ym = y + hEllipse / 2, // y-middle
  8239. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  8240. yeb = y + h; // y-end, bottom ellipse
  8241. this.beginPath();
  8242. this.moveTo(xe, ym);
  8243. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  8244. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  8245. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  8246. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  8247. this.lineTo(xe, ymb);
  8248. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  8249. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  8250. this.lineTo(x, ym);
  8251. };
  8252. /**
  8253. * Draw an arrow point (no line)
  8254. */
  8255. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  8256. // tail
  8257. var xt = x - length * Math.cos(angle);
  8258. var yt = y - length * Math.sin(angle);
  8259. // inner tail
  8260. // TODO: allow to customize different shapes
  8261. var xi = x - length * 0.9 * Math.cos(angle);
  8262. var yi = y - length * 0.9 * Math.sin(angle);
  8263. // left
  8264. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  8265. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  8266. // right
  8267. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  8268. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  8269. this.beginPath();
  8270. this.moveTo(x, y);
  8271. this.lineTo(xl, yl);
  8272. this.lineTo(xi, yi);
  8273. this.lineTo(xr, yr);
  8274. this.closePath();
  8275. };
  8276. /**
  8277. * Sets up the dashedLine functionality for drawing
  8278. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  8279. * @author David Jordan
  8280. * @date 2012-08-08
  8281. */
  8282. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  8283. if (!dashArray) dashArray=[10,5];
  8284. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  8285. var dashCount = dashArray.length;
  8286. this.moveTo(x, y);
  8287. var dx = (x2-x), dy = (y2-y);
  8288. var slope = dy/dx;
  8289. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  8290. var dashIndex=0, draw=true;
  8291. while (distRemaining>=0.1){
  8292. var dashLength = dashArray[dashIndex++%dashCount];
  8293. if (dashLength > distRemaining) dashLength = distRemaining;
  8294. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  8295. if (dx<0) xStep = -xStep;
  8296. x += xStep;
  8297. y += slope*xStep;
  8298. this[draw ? 'lineTo' : 'moveTo'](x,y);
  8299. distRemaining -= dashLength;
  8300. draw = !draw;
  8301. }
  8302. };
  8303. // TODO: add diamond shape
  8304. }
  8305. /**
  8306. * @class Node
  8307. * A node. A node can be connected to other nodes via one or multiple edges.
  8308. * @param {object} properties An object containing properties for the node. All
  8309. * properties are optional, except for the id.
  8310. * {number} id Id of the node. Required
  8311. * {string} label Text label for the node
  8312. * {number} x Horizontal position of the node
  8313. * {number} y Vertical position of the node
  8314. * {string} shape Node shape, available:
  8315. * "database", "circle", "ellipse",
  8316. * "box", "image", "text", "dot",
  8317. * "star", "triangle", "triangleDown",
  8318. * "square"
  8319. * {string} image An image url
  8320. * {string} title An title text, can be HTML
  8321. * {anytype} group A group name or number
  8322. * @param {Graph.Images} imagelist A list with images. Only needed
  8323. * when the node has an image
  8324. * @param {Graph.Groups} grouplist A list with groups. Needed for
  8325. * retrieving group properties
  8326. * @param {Object} constants An object with default values for
  8327. * example for the color
  8328. *
  8329. */
  8330. function Node(properties, imagelist, grouplist, constants) {
  8331. this.selected = false;
  8332. this.edges = []; // all edges connected to this node
  8333. this.dynamicEdges = [];
  8334. this.reroutedEdges = {};
  8335. this.group = constants.nodes.group;
  8336. this.fontSize = constants.nodes.fontSize;
  8337. this.fontFace = constants.nodes.fontFace;
  8338. this.fontColor = constants.nodes.fontColor;
  8339. this.fontDrawThreshold = 3;
  8340. this.color = constants.nodes.color;
  8341. // set defaults for the properties
  8342. this.id = undefined;
  8343. this.shape = constants.nodes.shape;
  8344. this.image = constants.nodes.image;
  8345. this.x = null;
  8346. this.y = null;
  8347. this.xFixed = false;
  8348. this.yFixed = false;
  8349. this.horizontalAlignLeft = true; // these are for the navigation controls
  8350. this.verticalAlignTop = true; // these are for the navigation controls
  8351. this.radius = constants.nodes.radius;
  8352. this.baseRadiusValue = constants.nodes.radius;
  8353. this.radiusFixed = false;
  8354. this.radiusMin = constants.nodes.radiusMin;
  8355. this.radiusMax = constants.nodes.radiusMax;
  8356. this.level = -1;
  8357. this.preassignedLevel = false;
  8358. this.imagelist = imagelist;
  8359. this.grouplist = grouplist;
  8360. // physics properties
  8361. this.fx = 0.0; // external force x
  8362. this.fy = 0.0; // external force y
  8363. this.vx = 0.0; // velocity x
  8364. this.vy = 0.0; // velocity y
  8365. this.minForce = constants.minForce;
  8366. this.damping = constants.physics.damping;
  8367. this.mass = 1; // kg
  8368. this.fixedData = {x:null,y:null};
  8369. this.setProperties(properties, constants);
  8370. // creating the variables for clustering
  8371. this.resetCluster();
  8372. this.dynamicEdgesLength = 0;
  8373. this.clusterSession = 0;
  8374. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  8375. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  8376. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  8377. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  8378. this.growthIndicator = 0;
  8379. // variables to tell the node about the graph.
  8380. this.graphScaleInv = 1;
  8381. this.graphScale = 1;
  8382. this.canvasTopLeft = {"x": -300, "y": -300};
  8383. this.canvasBottomRight = {"x": 300, "y": 300};
  8384. this.parentEdgeId = null;
  8385. }
  8386. /**
  8387. * (re)setting the clustering variables and objects
  8388. */
  8389. Node.prototype.resetCluster = function() {
  8390. // clustering variables
  8391. this.formationScale = undefined; // this is used to determine when to open the cluster
  8392. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  8393. this.containedNodes = {};
  8394. this.containedEdges = {};
  8395. this.clusterSessions = [];
  8396. };
  8397. /**
  8398. * Attach a edge to the node
  8399. * @param {Edge} edge
  8400. */
  8401. Node.prototype.attachEdge = function(edge) {
  8402. if (this.edges.indexOf(edge) == -1) {
  8403. this.edges.push(edge);
  8404. }
  8405. if (this.dynamicEdges.indexOf(edge) == -1) {
  8406. this.dynamicEdges.push(edge);
  8407. }
  8408. this.dynamicEdgesLength = this.dynamicEdges.length;
  8409. };
  8410. /**
  8411. * Detach a edge from the node
  8412. * @param {Edge} edge
  8413. */
  8414. Node.prototype.detachEdge = function(edge) {
  8415. var index = this.edges.indexOf(edge);
  8416. if (index != -1) {
  8417. this.edges.splice(index, 1);
  8418. this.dynamicEdges.splice(index, 1);
  8419. }
  8420. this.dynamicEdgesLength = this.dynamicEdges.length;
  8421. };
  8422. /**
  8423. * Set or overwrite properties for the node
  8424. * @param {Object} properties an object with properties
  8425. * @param {Object} constants and object with default, global properties
  8426. */
  8427. Node.prototype.setProperties = function(properties, constants) {
  8428. if (!properties) {
  8429. return;
  8430. }
  8431. this.originalLabel = undefined;
  8432. // basic properties
  8433. if (properties.id !== undefined) {this.id = properties.id;}
  8434. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  8435. if (properties.title !== undefined) {this.title = properties.title;}
  8436. if (properties.group !== undefined) {this.group = properties.group;}
  8437. if (properties.x !== undefined) {this.x = properties.x;}
  8438. if (properties.y !== undefined) {this.y = properties.y;}
  8439. if (properties.value !== undefined) {this.value = properties.value;}
  8440. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  8441. // physics
  8442. if (properties.mass !== undefined) {this.mass = properties.mass;}
  8443. // navigation controls properties
  8444. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  8445. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  8446. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  8447. if (this.id === undefined) {
  8448. throw "Node must have an id";
  8449. }
  8450. // copy group properties
  8451. if (this.group) {
  8452. var groupObj = this.grouplist.get(this.group);
  8453. for (var prop in groupObj) {
  8454. if (groupObj.hasOwnProperty(prop)) {
  8455. this[prop] = groupObj[prop];
  8456. }
  8457. }
  8458. }
  8459. // individual shape properties
  8460. if (properties.shape !== undefined) {this.shape = properties.shape;}
  8461. if (properties.image !== undefined) {this.image = properties.image;}
  8462. if (properties.radius !== undefined) {this.radius = properties.radius;}
  8463. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  8464. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8465. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8466. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8467. if (this.image !== undefined && this.image != "") {
  8468. if (this.imagelist) {
  8469. this.imageObj = this.imagelist.load(this.image);
  8470. }
  8471. else {
  8472. throw "No imagelist provided";
  8473. }
  8474. }
  8475. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  8476. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  8477. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  8478. if (this.shape == 'image') {
  8479. this.radiusMin = constants.nodes.widthMin;
  8480. this.radiusMax = constants.nodes.widthMax;
  8481. }
  8482. // choose draw method depending on the shape
  8483. switch (this.shape) {
  8484. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  8485. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  8486. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  8487. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8488. // TODO: add diamond shape
  8489. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  8490. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  8491. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  8492. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  8493. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  8494. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  8495. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  8496. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8497. }
  8498. // reset the size of the node, this can be changed
  8499. this._reset();
  8500. };
  8501. /**
  8502. * select this node
  8503. */
  8504. Node.prototype.select = function() {
  8505. this.selected = true;
  8506. this._reset();
  8507. };
  8508. /**
  8509. * unselect this node
  8510. */
  8511. Node.prototype.unselect = function() {
  8512. this.selected = false;
  8513. this._reset();
  8514. };
  8515. /**
  8516. * Reset the calculated size of the node, forces it to recalculate its size
  8517. */
  8518. Node.prototype.clearSizeCache = function() {
  8519. this._reset();
  8520. };
  8521. /**
  8522. * Reset the calculated size of the node, forces it to recalculate its size
  8523. * @private
  8524. */
  8525. Node.prototype._reset = function() {
  8526. this.width = undefined;
  8527. this.height = undefined;
  8528. };
  8529. /**
  8530. * get the title of this node.
  8531. * @return {string} title The title of the node, or undefined when no title
  8532. * has been set.
  8533. */
  8534. Node.prototype.getTitle = function() {
  8535. return typeof this.title === "function" ? this.title() : this.title;
  8536. };
  8537. /**
  8538. * Calculate the distance to the border of the Node
  8539. * @param {CanvasRenderingContext2D} ctx
  8540. * @param {Number} angle Angle in radians
  8541. * @returns {number} distance Distance to the border in pixels
  8542. */
  8543. Node.prototype.distanceToBorder = function (ctx, angle) {
  8544. var borderWidth = 1;
  8545. if (!this.width) {
  8546. this.resize(ctx);
  8547. }
  8548. switch (this.shape) {
  8549. case 'circle':
  8550. case 'dot':
  8551. return this.radius + borderWidth;
  8552. case 'ellipse':
  8553. var a = this.width / 2;
  8554. var b = this.height / 2;
  8555. var w = (Math.sin(angle) * a);
  8556. var h = (Math.cos(angle) * b);
  8557. return a * b / Math.sqrt(w * w + h * h);
  8558. // TODO: implement distanceToBorder for database
  8559. // TODO: implement distanceToBorder for triangle
  8560. // TODO: implement distanceToBorder for triangleDown
  8561. case 'box':
  8562. case 'image':
  8563. case 'text':
  8564. default:
  8565. if (this.width) {
  8566. return Math.min(
  8567. Math.abs(this.width / 2 / Math.cos(angle)),
  8568. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  8569. // TODO: reckon with border radius too in case of box
  8570. }
  8571. else {
  8572. return 0;
  8573. }
  8574. }
  8575. // TODO: implement calculation of distance to border for all shapes
  8576. };
  8577. /**
  8578. * Set forces acting on the node
  8579. * @param {number} fx Force in horizontal direction
  8580. * @param {number} fy Force in vertical direction
  8581. */
  8582. Node.prototype._setForce = function(fx, fy) {
  8583. this.fx = fx;
  8584. this.fy = fy;
  8585. };
  8586. /**
  8587. * Add forces acting on the node
  8588. * @param {number} fx Force in horizontal direction
  8589. * @param {number} fy Force in vertical direction
  8590. * @private
  8591. */
  8592. Node.prototype._addForce = function(fx, fy) {
  8593. this.fx += fx;
  8594. this.fy += fy;
  8595. };
  8596. /**
  8597. * Perform one discrete step for the node
  8598. * @param {number} interval Time interval in seconds
  8599. */
  8600. Node.prototype.discreteStep = function(interval) {
  8601. if (!this.xFixed) {
  8602. var dx = this.damping * this.vx; // damping force
  8603. var ax = (this.fx - dx) / this.mass; // acceleration
  8604. this.vx += ax * interval; // velocity
  8605. this.x += this.vx * interval; // position
  8606. }
  8607. if (!this.yFixed) {
  8608. var dy = this.damping * this.vy; // damping force
  8609. var ay = (this.fy - dy) / this.mass; // acceleration
  8610. this.vy += ay * interval; // velocity
  8611. this.y += this.vy * interval; // position
  8612. }
  8613. };
  8614. /**
  8615. * Perform one discrete step for the node
  8616. * @param {number} interval Time interval in seconds
  8617. */
  8618. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  8619. if (!this.xFixed) {
  8620. var dx = this.damping * this.vx; // damping force
  8621. var ax = (this.fx - dx) / this.mass; // acceleration
  8622. this.vx += ax * interval; // velocity
  8623. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  8624. this.x += this.vx * interval; // position
  8625. }
  8626. else {
  8627. this.fx = 0;
  8628. }
  8629. if (!this.yFixed) {
  8630. var dy = this.damping * this.vy; // damping force
  8631. var ay = (this.fy - dy) / this.mass; // acceleration
  8632. this.vy += ay * interval; // velocity
  8633. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  8634. this.y += this.vy * interval; // position
  8635. }
  8636. else {
  8637. this.fy = 0;
  8638. }
  8639. };
  8640. /**
  8641. * Check if this node has a fixed x and y position
  8642. * @return {boolean} true if fixed, false if not
  8643. */
  8644. Node.prototype.isFixed = function() {
  8645. return (this.xFixed && this.yFixed);
  8646. };
  8647. /**
  8648. * Check if this node is moving
  8649. * @param {number} vmin the minimum velocity considered as "moving"
  8650. * @return {boolean} true if moving, false if it has no velocity
  8651. */
  8652. // TODO: replace this method with calculating the kinetic energy
  8653. Node.prototype.isMoving = function(vmin) {
  8654. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  8655. };
  8656. /**
  8657. * check if this node is selecte
  8658. * @return {boolean} selected True if node is selected, else false
  8659. */
  8660. Node.prototype.isSelected = function() {
  8661. return this.selected;
  8662. };
  8663. /**
  8664. * Retrieve the value of the node. Can be undefined
  8665. * @return {Number} value
  8666. */
  8667. Node.prototype.getValue = function() {
  8668. return this.value;
  8669. };
  8670. /**
  8671. * Calculate the distance from the nodes location to the given location (x,y)
  8672. * @param {Number} x
  8673. * @param {Number} y
  8674. * @return {Number} value
  8675. */
  8676. Node.prototype.getDistance = function(x, y) {
  8677. var dx = this.x - x,
  8678. dy = this.y - y;
  8679. return Math.sqrt(dx * dx + dy * dy);
  8680. };
  8681. /**
  8682. * Adjust the value range of the node. The node will adjust it's radius
  8683. * based on its value.
  8684. * @param {Number} min
  8685. * @param {Number} max
  8686. */
  8687. Node.prototype.setValueRange = function(min, max) {
  8688. if (!this.radiusFixed && this.value !== undefined) {
  8689. if (max == min) {
  8690. this.radius = (this.radiusMin + this.radiusMax) / 2;
  8691. }
  8692. else {
  8693. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8694. this.radius = (this.value - min) * scale + this.radiusMin;
  8695. }
  8696. }
  8697. this.baseRadiusValue = this.radius;
  8698. };
  8699. /**
  8700. * Draw this node in the given canvas
  8701. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8702. * @param {CanvasRenderingContext2D} ctx
  8703. */
  8704. Node.prototype.draw = function(ctx) {
  8705. throw "Draw method not initialized for node";
  8706. };
  8707. /**
  8708. * Recalculate the size of this node in the given canvas
  8709. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8710. * @param {CanvasRenderingContext2D} ctx
  8711. */
  8712. Node.prototype.resize = function(ctx) {
  8713. throw "Resize method not initialized for node";
  8714. };
  8715. /**
  8716. * Check if this object is overlapping with the provided object
  8717. * @param {Object} obj an object with parameters left, top, right, bottom
  8718. * @return {boolean} True if location is located on node
  8719. */
  8720. Node.prototype.isOverlappingWith = function(obj) {
  8721. return (this.left < obj.right &&
  8722. this.left + this.width > obj.left &&
  8723. this.top < obj.bottom &&
  8724. this.top + this.height > obj.top);
  8725. };
  8726. Node.prototype._resizeImage = function (ctx) {
  8727. // TODO: pre calculate the image size
  8728. if (!this.width || !this.height) { // undefined or 0
  8729. var width, height;
  8730. if (this.value) {
  8731. this.radius = this.baseRadiusValue;
  8732. var scale = this.imageObj.height / this.imageObj.width;
  8733. if (scale !== undefined) {
  8734. width = this.radius || this.imageObj.width;
  8735. height = this.radius * scale || this.imageObj.height;
  8736. }
  8737. else {
  8738. width = 0;
  8739. height = 0;
  8740. }
  8741. }
  8742. else {
  8743. width = this.imageObj.width;
  8744. height = this.imageObj.height;
  8745. }
  8746. this.width = width;
  8747. this.height = height;
  8748. this.growthIndicator = 0;
  8749. if (this.width > 0 && this.height > 0) {
  8750. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8751. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8752. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8753. this.growthIndicator = this.width - width;
  8754. }
  8755. }
  8756. };
  8757. Node.prototype._drawImage = function (ctx) {
  8758. this._resizeImage(ctx);
  8759. this.left = this.x - this.width / 2;
  8760. this.top = this.y - this.height / 2;
  8761. var yLabel;
  8762. if (this.imageObj.width != 0 ) {
  8763. // draw the shade
  8764. if (this.clusterSize > 1) {
  8765. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  8766. lineWidth *= this.graphScaleInv;
  8767. lineWidth = Math.min(0.2 * this.width,lineWidth);
  8768. ctx.globalAlpha = 0.5;
  8769. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  8770. }
  8771. // draw the image
  8772. ctx.globalAlpha = 1.0;
  8773. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8774. yLabel = this.y + this.height / 2;
  8775. }
  8776. else {
  8777. // image still loading... just draw the label for now
  8778. yLabel = this.y;
  8779. }
  8780. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8781. };
  8782. Node.prototype._resizeBox = function (ctx) {
  8783. if (!this.width) {
  8784. var margin = 5;
  8785. var textSize = this.getTextSize(ctx);
  8786. this.width = textSize.width + 2 * margin;
  8787. this.height = textSize.height + 2 * margin;
  8788. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8789. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8790. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8791. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8792. }
  8793. };
  8794. Node.prototype._drawBox = function (ctx) {
  8795. this._resizeBox(ctx);
  8796. this.left = this.x - this.width / 2;
  8797. this.top = this.y - this.height / 2;
  8798. var clusterLineWidth = 2.5;
  8799. var selectionLineWidth = 2;
  8800. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8801. // draw the outer border
  8802. if (this.clusterSize > 1) {
  8803. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8804. ctx.lineWidth *= this.graphScaleInv;
  8805. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8806. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  8807. ctx.stroke();
  8808. }
  8809. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8810. ctx.lineWidth *= this.graphScaleInv;
  8811. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8812. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8813. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8814. ctx.fill();
  8815. ctx.stroke();
  8816. this._label(ctx, this.label, this.x, this.y);
  8817. };
  8818. Node.prototype._resizeDatabase = function (ctx) {
  8819. if (!this.width) {
  8820. var margin = 5;
  8821. var textSize = this.getTextSize(ctx);
  8822. var size = textSize.width + 2 * margin;
  8823. this.width = size;
  8824. this.height = size;
  8825. // scaling used for clustering
  8826. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8827. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8828. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8829. this.growthIndicator = this.width - size;
  8830. }
  8831. };
  8832. Node.prototype._drawDatabase = function (ctx) {
  8833. this._resizeDatabase(ctx);
  8834. this.left = this.x - this.width / 2;
  8835. this.top = this.y - this.height / 2;
  8836. var clusterLineWidth = 2.5;
  8837. var selectionLineWidth = 2;
  8838. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8839. // draw the outer border
  8840. if (this.clusterSize > 1) {
  8841. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8842. ctx.lineWidth *= this.graphScaleInv;
  8843. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8844. 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);
  8845. ctx.stroke();
  8846. }
  8847. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8848. ctx.lineWidth *= this.graphScaleInv;
  8849. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8850. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8851. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8852. ctx.fill();
  8853. ctx.stroke();
  8854. this._label(ctx, this.label, this.x, this.y);
  8855. };
  8856. Node.prototype._resizeCircle = function (ctx) {
  8857. if (!this.width) {
  8858. var margin = 5;
  8859. var textSize = this.getTextSize(ctx);
  8860. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8861. this.radius = diameter / 2;
  8862. this.width = diameter;
  8863. this.height = diameter;
  8864. // scaling used for clustering
  8865. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8866. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8867. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8868. this.growthIndicator = this.radius - 0.5*diameter;
  8869. }
  8870. };
  8871. Node.prototype._drawCircle = function (ctx) {
  8872. this._resizeCircle(ctx);
  8873. this.left = this.x - this.width / 2;
  8874. this.top = this.y - this.height / 2;
  8875. var clusterLineWidth = 2.5;
  8876. var selectionLineWidth = 2;
  8877. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8878. // draw the outer border
  8879. if (this.clusterSize > 1) {
  8880. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8881. ctx.lineWidth *= this.graphScaleInv;
  8882. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8883. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  8884. ctx.stroke();
  8885. }
  8886. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8887. ctx.lineWidth *= this.graphScaleInv;
  8888. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8889. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8890. ctx.circle(this.x, this.y, this.radius);
  8891. ctx.fill();
  8892. ctx.stroke();
  8893. this._label(ctx, this.label, this.x, this.y);
  8894. };
  8895. Node.prototype._resizeEllipse = function (ctx) {
  8896. if (!this.width) {
  8897. var textSize = this.getTextSize(ctx);
  8898. this.width = textSize.width * 1.5;
  8899. this.height = textSize.height * 2;
  8900. if (this.width < this.height) {
  8901. this.width = this.height;
  8902. }
  8903. var defaultSize = this.width;
  8904. // scaling used for clustering
  8905. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8906. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8907. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8908. this.growthIndicator = this.width - defaultSize;
  8909. }
  8910. };
  8911. Node.prototype._drawEllipse = function (ctx) {
  8912. this._resizeEllipse(ctx);
  8913. this.left = this.x - this.width / 2;
  8914. this.top = this.y - this.height / 2;
  8915. var clusterLineWidth = 2.5;
  8916. var selectionLineWidth = 2;
  8917. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8918. // draw the outer border
  8919. if (this.clusterSize > 1) {
  8920. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8921. ctx.lineWidth *= this.graphScaleInv;
  8922. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8923. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  8924. ctx.stroke();
  8925. }
  8926. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8927. ctx.lineWidth *= this.graphScaleInv;
  8928. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8929. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8930. ctx.ellipse(this.left, this.top, this.width, this.height);
  8931. ctx.fill();
  8932. ctx.stroke();
  8933. this._label(ctx, this.label, this.x, this.y);
  8934. };
  8935. Node.prototype._drawDot = function (ctx) {
  8936. this._drawShape(ctx, 'circle');
  8937. };
  8938. Node.prototype._drawTriangle = function (ctx) {
  8939. this._drawShape(ctx, 'triangle');
  8940. };
  8941. Node.prototype._drawTriangleDown = function (ctx) {
  8942. this._drawShape(ctx, 'triangleDown');
  8943. };
  8944. Node.prototype._drawSquare = function (ctx) {
  8945. this._drawShape(ctx, 'square');
  8946. };
  8947. Node.prototype._drawStar = function (ctx) {
  8948. this._drawShape(ctx, 'star');
  8949. };
  8950. Node.prototype._resizeShape = function (ctx) {
  8951. if (!this.width) {
  8952. this.radius = this.baseRadiusValue;
  8953. var size = 2 * this.radius;
  8954. this.width = size;
  8955. this.height = size;
  8956. // scaling used for clustering
  8957. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8958. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8959. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8960. this.growthIndicator = this.width - size;
  8961. }
  8962. };
  8963. Node.prototype._drawShape = function (ctx, shape) {
  8964. this._resizeShape(ctx);
  8965. this.left = this.x - this.width / 2;
  8966. this.top = this.y - this.height / 2;
  8967. var clusterLineWidth = 2.5;
  8968. var selectionLineWidth = 2;
  8969. var radiusMultiplier = 2;
  8970. // choose draw method depending on the shape
  8971. switch (shape) {
  8972. case 'dot': radiusMultiplier = 2; break;
  8973. case 'square': radiusMultiplier = 2; break;
  8974. case 'triangle': radiusMultiplier = 3; break;
  8975. case 'triangleDown': radiusMultiplier = 3; break;
  8976. case 'star': radiusMultiplier = 4; break;
  8977. }
  8978. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8979. // draw the outer border
  8980. if (this.clusterSize > 1) {
  8981. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8982. ctx.lineWidth *= this.graphScaleInv;
  8983. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8984. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  8985. ctx.stroke();
  8986. }
  8987. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8988. ctx.lineWidth *= this.graphScaleInv;
  8989. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8990. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8991. ctx[shape](this.x, this.y, this.radius);
  8992. ctx.fill();
  8993. ctx.stroke();
  8994. if (this.label) {
  8995. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8996. }
  8997. };
  8998. Node.prototype._resizeText = function (ctx) {
  8999. if (!this.width) {
  9000. var margin = 5;
  9001. var textSize = this.getTextSize(ctx);
  9002. this.width = textSize.width + 2 * margin;
  9003. this.height = textSize.height + 2 * margin;
  9004. // scaling used for clustering
  9005. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  9006. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  9007. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  9008. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  9009. }
  9010. };
  9011. Node.prototype._drawText = function (ctx) {
  9012. this._resizeText(ctx);
  9013. this.left = this.x - this.width / 2;
  9014. this.top = this.y - this.height / 2;
  9015. this._label(ctx, this.label, this.x, this.y);
  9016. };
  9017. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  9018. if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
  9019. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  9020. ctx.fillStyle = this.fontColor || "black";
  9021. ctx.textAlign = align || "center";
  9022. ctx.textBaseline = baseline || "middle";
  9023. var lines = text.split('\n'),
  9024. lineCount = lines.length,
  9025. fontSize = (this.fontSize + 4),
  9026. yLine = y + (1 - lineCount) / 2 * fontSize;
  9027. for (var i = 0; i < lineCount; i++) {
  9028. ctx.fillText(lines[i], x, yLine);
  9029. yLine += fontSize;
  9030. }
  9031. }
  9032. };
  9033. Node.prototype.getTextSize = function(ctx) {
  9034. if (this.label !== undefined) {
  9035. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  9036. var lines = this.label.split('\n'),
  9037. height = (this.fontSize + 4) * lines.length,
  9038. width = 0;
  9039. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  9040. width = Math.max(width, ctx.measureText(lines[i]).width);
  9041. }
  9042. return {"width": width, "height": height};
  9043. }
  9044. else {
  9045. return {"width": 0, "height": 0};
  9046. }
  9047. };
  9048. /**
  9049. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  9050. * there is a safety margin of 0.3 * width;
  9051. *
  9052. * @returns {boolean}
  9053. */
  9054. Node.prototype.inArea = function() {
  9055. if (this.width !== undefined) {
  9056. return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
  9057. this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
  9058. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  9059. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  9060. }
  9061. else {
  9062. return true;
  9063. }
  9064. };
  9065. /**
  9066. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  9067. * @returns {boolean}
  9068. */
  9069. Node.prototype.inView = function() {
  9070. return (this.x >= this.canvasTopLeft.x &&
  9071. this.x < this.canvasBottomRight.x &&
  9072. this.y >= this.canvasTopLeft.y &&
  9073. this.y < this.canvasBottomRight.y);
  9074. };
  9075. /**
  9076. * This allows the zoom level of the graph to influence the rendering
  9077. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  9078. *
  9079. * @param scale
  9080. * @param canvasTopLeft
  9081. * @param canvasBottomRight
  9082. */
  9083. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  9084. this.graphScaleInv = 1.0/scale;
  9085. this.graphScale = scale;
  9086. this.canvasTopLeft = canvasTopLeft;
  9087. this.canvasBottomRight = canvasBottomRight;
  9088. };
  9089. /**
  9090. * This allows the zoom level of the graph to influence the rendering
  9091. *
  9092. * @param scale
  9093. */
  9094. Node.prototype.setScale = function(scale) {
  9095. this.graphScaleInv = 1.0/scale;
  9096. this.graphScale = scale;
  9097. };
  9098. /**
  9099. * set the velocity at 0. Is called when this node is contained in another during clustering
  9100. */
  9101. Node.prototype.clearVelocity = function() {
  9102. this.vx = 0;
  9103. this.vy = 0;
  9104. };
  9105. /**
  9106. * Basic preservation of (kinectic) energy
  9107. *
  9108. * @param massBeforeClustering
  9109. */
  9110. Node.prototype.updateVelocity = function(massBeforeClustering) {
  9111. var energyBefore = this.vx * this.vx * massBeforeClustering;
  9112. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  9113. this.vx = Math.sqrt(energyBefore/this.mass);
  9114. energyBefore = this.vy * this.vy * massBeforeClustering;
  9115. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  9116. this.vy = Math.sqrt(energyBefore/this.mass);
  9117. };
  9118. /**
  9119. * @class Edge
  9120. *
  9121. * A edge connects two nodes
  9122. * @param {Object} properties Object with properties. Must contain
  9123. * At least properties from and to.
  9124. * Available properties: from (number),
  9125. * to (number), label (string, color (string),
  9126. * width (number), style (string),
  9127. * length (number), title (string)
  9128. * @param {Graph} graph A graph object, used to find and edge to
  9129. * nodes.
  9130. * @param {Object} constants An object with default values for
  9131. * example for the color
  9132. */
  9133. function Edge (properties, graph, constants) {
  9134. if (!graph) {
  9135. throw "No graph provided";
  9136. }
  9137. this.graph = graph;
  9138. // initialize constants
  9139. this.widthMin = constants.edges.widthMin;
  9140. this.widthMax = constants.edges.widthMax;
  9141. // initialize variables
  9142. this.id = undefined;
  9143. this.fromId = undefined;
  9144. this.toId = undefined;
  9145. this.style = constants.edges.style;
  9146. this.title = undefined;
  9147. this.width = constants.edges.width;
  9148. this.value = undefined;
  9149. this.length = constants.physics.springLength;
  9150. this.customLength = false;
  9151. this.selected = false;
  9152. this.smooth = constants.smoothCurves;
  9153. this.from = null; // a node
  9154. this.to = null; // a node
  9155. this.via = null; // a temp node
  9156. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  9157. // by storing the original information we can revert to the original connection when the cluser is opened.
  9158. this.originalFromId = [];
  9159. this.originalToId = [];
  9160. this.connected = false;
  9161. // Added to support dashed lines
  9162. // David Jordan
  9163. // 2012-08-08
  9164. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  9165. this.color = {color:constants.edges.color.color,
  9166. highlight:constants.edges.color.highlight};
  9167. this.widthFixed = false;
  9168. this.lengthFixed = false;
  9169. this.setProperties(properties, constants);
  9170. }
  9171. /**
  9172. * Set or overwrite properties for the edge
  9173. * @param {Object} properties an object with properties
  9174. * @param {Object} constants and object with default, global properties
  9175. */
  9176. Edge.prototype.setProperties = function(properties, constants) {
  9177. if (!properties) {
  9178. return;
  9179. }
  9180. if (properties.from !== undefined) {this.fromId = properties.from;}
  9181. if (properties.to !== undefined) {this.toId = properties.to;}
  9182. if (properties.id !== undefined) {this.id = properties.id;}
  9183. if (properties.style !== undefined) {this.style = properties.style;}
  9184. if (properties.label !== undefined) {this.label = properties.label;}
  9185. if (this.label) {
  9186. this.fontSize = constants.edges.fontSize;
  9187. this.fontFace = constants.edges.fontFace;
  9188. this.fontColor = constants.edges.fontColor;
  9189. this.fontFill = constants.edges.fontFill;
  9190. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  9191. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  9192. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  9193. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  9194. }
  9195. if (properties.title !== undefined) {this.title = properties.title;}
  9196. if (properties.width !== undefined) {this.width = properties.width;}
  9197. if (properties.value !== undefined) {this.value = properties.value;}
  9198. if (properties.length !== undefined) {this.length = properties.length;
  9199. this.customLength = true;}
  9200. // Added to support dashed lines
  9201. // David Jordan
  9202. // 2012-08-08
  9203. if (properties.dash) {
  9204. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  9205. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  9206. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  9207. }
  9208. if (properties.color !== undefined) {
  9209. if (util.isString(properties.color)) {
  9210. this.color.color = properties.color;
  9211. this.color.highlight = properties.color;
  9212. }
  9213. else {
  9214. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  9215. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  9216. }
  9217. }
  9218. // A node is connected when it has a from and to node.
  9219. this.connect();
  9220. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  9221. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  9222. // set draw method based on style
  9223. switch (this.style) {
  9224. case 'line': this.draw = this._drawLine; break;
  9225. case 'arrow': this.draw = this._drawArrow; break;
  9226. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  9227. case 'dash-line': this.draw = this._drawDashLine; break;
  9228. default: this.draw = this._drawLine; break;
  9229. }
  9230. };
  9231. /**
  9232. * Connect an edge to its nodes
  9233. */
  9234. Edge.prototype.connect = function () {
  9235. this.disconnect();
  9236. this.from = this.graph.nodes[this.fromId] || null;
  9237. this.to = this.graph.nodes[this.toId] || null;
  9238. this.connected = (this.from && this.to);
  9239. if (this.connected) {
  9240. this.from.attachEdge(this);
  9241. this.to.attachEdge(this);
  9242. }
  9243. else {
  9244. if (this.from) {
  9245. this.from.detachEdge(this);
  9246. }
  9247. if (this.to) {
  9248. this.to.detachEdge(this);
  9249. }
  9250. }
  9251. };
  9252. /**
  9253. * Disconnect an edge from its nodes
  9254. */
  9255. Edge.prototype.disconnect = function () {
  9256. if (this.from) {
  9257. this.from.detachEdge(this);
  9258. this.from = null;
  9259. }
  9260. if (this.to) {
  9261. this.to.detachEdge(this);
  9262. this.to = null;
  9263. }
  9264. this.connected = false;
  9265. };
  9266. /**
  9267. * get the title of this edge.
  9268. * @return {string} title The title of the edge, or undefined when no title
  9269. * has been set.
  9270. */
  9271. Edge.prototype.getTitle = function() {
  9272. return typeof this.title === "function" ? this.title() : this.title;
  9273. };
  9274. /**
  9275. * Retrieve the value of the edge. Can be undefined
  9276. * @return {Number} value
  9277. */
  9278. Edge.prototype.getValue = function() {
  9279. return this.value;
  9280. };
  9281. /**
  9282. * Adjust the value range of the edge. The edge will adjust it's width
  9283. * based on its value.
  9284. * @param {Number} min
  9285. * @param {Number} max
  9286. */
  9287. Edge.prototype.setValueRange = function(min, max) {
  9288. if (!this.widthFixed && this.value !== undefined) {
  9289. var scale = (this.widthMax - this.widthMin) / (max - min);
  9290. this.width = (this.value - min) * scale + this.widthMin;
  9291. }
  9292. };
  9293. /**
  9294. * Redraw a edge
  9295. * Draw this edge in the given canvas
  9296. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9297. * @param {CanvasRenderingContext2D} ctx
  9298. */
  9299. Edge.prototype.draw = function(ctx) {
  9300. throw "Method draw not initialized in edge";
  9301. };
  9302. /**
  9303. * Check if this object is overlapping with the provided object
  9304. * @param {Object} obj an object with parameters left, top
  9305. * @return {boolean} True if location is located on the edge
  9306. */
  9307. Edge.prototype.isOverlappingWith = function(obj) {
  9308. if (this.connected) {
  9309. var distMax = 10;
  9310. var xFrom = this.from.x;
  9311. var yFrom = this.from.y;
  9312. var xTo = this.to.x;
  9313. var yTo = this.to.y;
  9314. var xObj = obj.left;
  9315. var yObj = obj.top;
  9316. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  9317. return (dist < distMax);
  9318. }
  9319. else {
  9320. return false
  9321. }
  9322. };
  9323. /**
  9324. * Redraw a edge as a line
  9325. * Draw this edge in the given canvas
  9326. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9327. * @param {CanvasRenderingContext2D} ctx
  9328. * @private
  9329. */
  9330. Edge.prototype._drawLine = function(ctx) {
  9331. // set style
  9332. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  9333. else {ctx.strokeStyle = this.color.color;}
  9334. ctx.lineWidth = this._getLineWidth();
  9335. if (this.from != this.to) {
  9336. // draw line
  9337. this._line(ctx);
  9338. // draw label
  9339. var point;
  9340. if (this.label) {
  9341. if (this.smooth == true) {
  9342. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9343. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9344. point = {x:midpointX, y:midpointY};
  9345. }
  9346. else {
  9347. point = this._pointOnLine(0.5);
  9348. }
  9349. this._label(ctx, this.label, point.x, point.y);
  9350. }
  9351. }
  9352. else {
  9353. var x, y;
  9354. var radius = this.length / 4;
  9355. var node = this.from;
  9356. if (!node.width) {
  9357. node.resize(ctx);
  9358. }
  9359. if (node.width > node.height) {
  9360. x = node.x + node.width / 2;
  9361. y = node.y - radius;
  9362. }
  9363. else {
  9364. x = node.x + radius;
  9365. y = node.y - node.height / 2;
  9366. }
  9367. this._circle(ctx, x, y, radius);
  9368. point = this._pointOnCircle(x, y, radius, 0.5);
  9369. this._label(ctx, this.label, point.x, point.y);
  9370. }
  9371. };
  9372. /**
  9373. * Get the line width of the edge. Depends on width and whether one of the
  9374. * connected nodes is selected.
  9375. * @return {Number} width
  9376. * @private
  9377. */
  9378. Edge.prototype._getLineWidth = function() {
  9379. if (this.selected == true) {
  9380. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  9381. }
  9382. else {
  9383. return this.width*this.graphScaleInv;
  9384. }
  9385. };
  9386. /**
  9387. * Draw a line between two nodes
  9388. * @param {CanvasRenderingContext2D} ctx
  9389. * @private
  9390. */
  9391. Edge.prototype._line = function (ctx) {
  9392. // draw a straight line
  9393. ctx.beginPath();
  9394. ctx.moveTo(this.from.x, this.from.y);
  9395. if (this.smooth == true) {
  9396. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9397. }
  9398. else {
  9399. ctx.lineTo(this.to.x, this.to.y);
  9400. }
  9401. ctx.stroke();
  9402. };
  9403. /**
  9404. * Draw a line from a node to itself, a circle
  9405. * @param {CanvasRenderingContext2D} ctx
  9406. * @param {Number} x
  9407. * @param {Number} y
  9408. * @param {Number} radius
  9409. * @private
  9410. */
  9411. Edge.prototype._circle = function (ctx, x, y, radius) {
  9412. // draw a circle
  9413. ctx.beginPath();
  9414. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9415. ctx.stroke();
  9416. };
  9417. /**
  9418. * Draw label with white background and with the middle at (x, y)
  9419. * @param {CanvasRenderingContext2D} ctx
  9420. * @param {String} text
  9421. * @param {Number} x
  9422. * @param {Number} y
  9423. * @private
  9424. */
  9425. Edge.prototype._label = function (ctx, text, x, y) {
  9426. if (text) {
  9427. // TODO: cache the calculated size
  9428. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  9429. this.fontSize + "px " + this.fontFace;
  9430. ctx.fillStyle = this.fontFill;
  9431. var width = ctx.measureText(text).width;
  9432. var height = this.fontSize;
  9433. var left = x - width / 2;
  9434. var top = y - height / 2;
  9435. ctx.fillRect(left, top, width, height);
  9436. // draw text
  9437. ctx.fillStyle = this.fontColor || "black";
  9438. ctx.textAlign = "left";
  9439. ctx.textBaseline = "top";
  9440. ctx.fillText(text, left, top);
  9441. }
  9442. };
  9443. /**
  9444. * Redraw a edge as a dashed line
  9445. * Draw this edge in the given canvas
  9446. * @author David Jordan
  9447. * @date 2012-08-08
  9448. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9449. * @param {CanvasRenderingContext2D} ctx
  9450. * @private
  9451. */
  9452. Edge.prototype._drawDashLine = function(ctx) {
  9453. // set style
  9454. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  9455. else {ctx.strokeStyle = this.color.color;}
  9456. ctx.lineWidth = this._getLineWidth();
  9457. // only firefox and chrome support this method, else we use the legacy one.
  9458. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  9459. ctx.beginPath();
  9460. ctx.moveTo(this.from.x, this.from.y);
  9461. // configure the dash pattern
  9462. var pattern = [0];
  9463. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  9464. pattern = [this.dash.length,this.dash.gap];
  9465. }
  9466. else {
  9467. pattern = [5,5];
  9468. }
  9469. // set dash settings for chrome or firefox
  9470. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9471. ctx.setLineDash(pattern);
  9472. ctx.lineDashOffset = 0;
  9473. } else { //Firefox
  9474. ctx.mozDash = pattern;
  9475. ctx.mozDashOffset = 0;
  9476. }
  9477. // draw the line
  9478. if (this.smooth == true) {
  9479. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9480. }
  9481. else {
  9482. ctx.lineTo(this.to.x, this.to.y);
  9483. }
  9484. ctx.stroke();
  9485. // restore the dash settings.
  9486. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9487. ctx.setLineDash([0]);
  9488. ctx.lineDashOffset = 0;
  9489. } else { //Firefox
  9490. ctx.mozDash = [0];
  9491. ctx.mozDashOffset = 0;
  9492. }
  9493. }
  9494. else { // unsupporting smooth lines
  9495. // draw dashed line
  9496. ctx.beginPath();
  9497. ctx.lineCap = 'round';
  9498. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  9499. {
  9500. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9501. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  9502. }
  9503. 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
  9504. {
  9505. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9506. [this.dash.length,this.dash.gap]);
  9507. }
  9508. else //If all else fails draw a line
  9509. {
  9510. ctx.moveTo(this.from.x, this.from.y);
  9511. ctx.lineTo(this.to.x, this.to.y);
  9512. }
  9513. ctx.stroke();
  9514. }
  9515. // draw label
  9516. if (this.label) {
  9517. var point;
  9518. if (this.smooth == true) {
  9519. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9520. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9521. point = {x:midpointX, y:midpointY};
  9522. }
  9523. else {
  9524. point = this._pointOnLine(0.5);
  9525. }
  9526. this._label(ctx, this.label, point.x, point.y);
  9527. }
  9528. };
  9529. /**
  9530. * Get a point on a line
  9531. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9532. * @return {Object} point
  9533. * @private
  9534. */
  9535. Edge.prototype._pointOnLine = function (percentage) {
  9536. return {
  9537. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  9538. y: (1 - percentage) * this.from.y + percentage * this.to.y
  9539. }
  9540. };
  9541. /**
  9542. * Get a point on a circle
  9543. * @param {Number} x
  9544. * @param {Number} y
  9545. * @param {Number} radius
  9546. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9547. * @return {Object} point
  9548. * @private
  9549. */
  9550. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  9551. var angle = (percentage - 3/8) * 2 * Math.PI;
  9552. return {
  9553. x: x + radius * Math.cos(angle),
  9554. y: y - radius * Math.sin(angle)
  9555. }
  9556. };
  9557. /**
  9558. * Redraw a edge as a line with an arrow halfway the line
  9559. * Draw this edge in the given canvas
  9560. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9561. * @param {CanvasRenderingContext2D} ctx
  9562. * @private
  9563. */
  9564. Edge.prototype._drawArrowCenter = function(ctx) {
  9565. var point;
  9566. // set style
  9567. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9568. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9569. ctx.lineWidth = this._getLineWidth();
  9570. if (this.from != this.to) {
  9571. // draw line
  9572. this._line(ctx);
  9573. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9574. var length = 10 + 5 * this.width; // TODO: make customizable?
  9575. // draw an arrow halfway the line
  9576. if (this.smooth == true) {
  9577. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9578. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9579. point = {x:midpointX, y:midpointY};
  9580. }
  9581. else {
  9582. point = this._pointOnLine(0.5);
  9583. }
  9584. ctx.arrow(point.x, point.y, angle, length);
  9585. ctx.fill();
  9586. ctx.stroke();
  9587. // draw label
  9588. if (this.label) {
  9589. this._label(ctx, this.label, point.x, point.y);
  9590. }
  9591. }
  9592. else {
  9593. // draw circle
  9594. var x, y;
  9595. var radius = 0.25 * Math.max(100,this.length);
  9596. var node = this.from;
  9597. if (!node.width) {
  9598. node.resize(ctx);
  9599. }
  9600. if (node.width > node.height) {
  9601. x = node.x + node.width * 0.5;
  9602. y = node.y - radius;
  9603. }
  9604. else {
  9605. x = node.x + radius;
  9606. y = node.y - node.height * 0.5;
  9607. }
  9608. this._circle(ctx, x, y, radius);
  9609. // draw all arrows
  9610. var angle = 0.2 * Math.PI;
  9611. var length = 10 + 5 * this.width; // TODO: make customizable?
  9612. point = this._pointOnCircle(x, y, radius, 0.5);
  9613. ctx.arrow(point.x, point.y, angle, length);
  9614. ctx.fill();
  9615. ctx.stroke();
  9616. // draw label
  9617. if (this.label) {
  9618. point = this._pointOnCircle(x, y, radius, 0.5);
  9619. this._label(ctx, this.label, point.x, point.y);
  9620. }
  9621. }
  9622. };
  9623. /**
  9624. * Redraw a edge as a line with an arrow
  9625. * Draw this edge in the given canvas
  9626. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9627. * @param {CanvasRenderingContext2D} ctx
  9628. * @private
  9629. */
  9630. Edge.prototype._drawArrow = function(ctx) {
  9631. // set style
  9632. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9633. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9634. ctx.lineWidth = this._getLineWidth();
  9635. var angle, length;
  9636. //draw a line
  9637. if (this.from != this.to) {
  9638. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9639. var dx = (this.to.x - this.from.x);
  9640. var dy = (this.to.y - this.from.y);
  9641. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9642. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  9643. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  9644. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  9645. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  9646. if (this.smooth == true) {
  9647. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  9648. dx = (this.to.x - this.via.x);
  9649. dy = (this.to.y - this.via.y);
  9650. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9651. }
  9652. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  9653. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  9654. var xTo,yTo;
  9655. if (this.smooth == true) {
  9656. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  9657. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  9658. }
  9659. else {
  9660. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  9661. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  9662. }
  9663. ctx.beginPath();
  9664. ctx.moveTo(xFrom,yFrom);
  9665. if (this.smooth == true) {
  9666. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  9667. }
  9668. else {
  9669. ctx.lineTo(xTo, yTo);
  9670. }
  9671. ctx.stroke();
  9672. // draw arrow at the end of the line
  9673. length = 10 + 5 * this.width;
  9674. ctx.arrow(xTo, yTo, angle, length);
  9675. ctx.fill();
  9676. ctx.stroke();
  9677. // draw label
  9678. if (this.label) {
  9679. var point;
  9680. if (this.smooth == true) {
  9681. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9682. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9683. point = {x:midpointX, y:midpointY};
  9684. }
  9685. else {
  9686. point = this._pointOnLine(0.5);
  9687. }
  9688. this._label(ctx, this.label, point.x, point.y);
  9689. }
  9690. }
  9691. else {
  9692. // draw circle
  9693. var node = this.from;
  9694. var x, y, arrow;
  9695. var radius = 0.25 * Math.max(100,this.length);
  9696. if (!node.width) {
  9697. node.resize(ctx);
  9698. }
  9699. if (node.width > node.height) {
  9700. x = node.x + node.width * 0.5;
  9701. y = node.y - radius;
  9702. arrow = {
  9703. x: x,
  9704. y: node.y,
  9705. angle: 0.9 * Math.PI
  9706. };
  9707. }
  9708. else {
  9709. x = node.x + radius;
  9710. y = node.y - node.height * 0.5;
  9711. arrow = {
  9712. x: node.x,
  9713. y: y,
  9714. angle: 0.6 * Math.PI
  9715. };
  9716. }
  9717. ctx.beginPath();
  9718. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9719. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9720. ctx.stroke();
  9721. // draw all arrows
  9722. length = 10 + 5 * this.width; // TODO: make customizable?
  9723. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9724. ctx.fill();
  9725. ctx.stroke();
  9726. // draw label
  9727. if (this.label) {
  9728. point = this._pointOnCircle(x, y, radius, 0.5);
  9729. this._label(ctx, this.label, point.x, point.y);
  9730. }
  9731. }
  9732. };
  9733. /**
  9734. * Calculate the distance between a point (x3,y3) and a line segment from
  9735. * (x1,y1) to (x2,y2).
  9736. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9737. * @param {number} x1
  9738. * @param {number} y1
  9739. * @param {number} x2
  9740. * @param {number} y2
  9741. * @param {number} x3
  9742. * @param {number} y3
  9743. * @private
  9744. */
  9745. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9746. if (this.smooth == true) {
  9747. var minDistance = 1e9;
  9748. var i,t,x,y,dx,dy;
  9749. for (i = 0; i < 10; i++) {
  9750. t = 0.1*i;
  9751. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  9752. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  9753. dx = Math.abs(x3-x);
  9754. dy = Math.abs(y3-y);
  9755. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  9756. }
  9757. return minDistance
  9758. }
  9759. else {
  9760. var px = x2-x1,
  9761. py = y2-y1,
  9762. something = px*px + py*py,
  9763. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9764. if (u > 1) {
  9765. u = 1;
  9766. }
  9767. else if (u < 0) {
  9768. u = 0;
  9769. }
  9770. var x = x1 + u * px,
  9771. y = y1 + u * py,
  9772. dx = x - x3,
  9773. dy = y - y3;
  9774. //# Note: If the actual distance does not matter,
  9775. //# if you only want to compare what this function
  9776. //# returns to other results of this function, you
  9777. //# can just return the squared distance instead
  9778. //# (i.e. remove the sqrt) to gain a little performance
  9779. return Math.sqrt(dx*dx + dy*dy);
  9780. }
  9781. };
  9782. /**
  9783. * This allows the zoom level of the graph to influence the rendering
  9784. *
  9785. * @param scale
  9786. */
  9787. Edge.prototype.setScale = function(scale) {
  9788. this.graphScaleInv = 1.0/scale;
  9789. };
  9790. Edge.prototype.select = function() {
  9791. this.selected = true;
  9792. };
  9793. Edge.prototype.unselect = function() {
  9794. this.selected = false;
  9795. };
  9796. Edge.prototype.positionBezierNode = function() {
  9797. if (this.via !== null) {
  9798. this.via.x = 0.5 * (this.from.x + this.to.x);
  9799. this.via.y = 0.5 * (this.from.y + this.to.y);
  9800. }
  9801. };
  9802. /**
  9803. * Popup is a class to create a popup window with some text
  9804. * @param {Element} container The container object.
  9805. * @param {Number} [x]
  9806. * @param {Number} [y]
  9807. * @param {String} [text]
  9808. * @param {Object} [style] An object containing borderColor,
  9809. * backgroundColor, etc.
  9810. */
  9811. function Popup(container, x, y, text, style) {
  9812. if (container) {
  9813. this.container = container;
  9814. }
  9815. else {
  9816. this.container = document.body;
  9817. }
  9818. // x, y and text are optional, see if a style object was passed in their place
  9819. if (style === undefined) {
  9820. if (typeof x === "object") {
  9821. style = x;
  9822. x = undefined;
  9823. } else if (typeof text === "object") {
  9824. style = text;
  9825. text = undefined;
  9826. } else {
  9827. // for backwards compatibility, in case clients other than Graph are creating Popup directly
  9828. style = {
  9829. fontColor: 'black',
  9830. fontSize: 14, // px
  9831. fontFace: 'verdana',
  9832. color: {
  9833. border: '#666',
  9834. background: '#FFFFC6'
  9835. }
  9836. }
  9837. }
  9838. }
  9839. this.x = 0;
  9840. this.y = 0;
  9841. this.padding = 5;
  9842. if (x !== undefined && y !== undefined ) {
  9843. this.setPosition(x, y);
  9844. }
  9845. if (text !== undefined) {
  9846. this.setText(text);
  9847. }
  9848. // create the frame
  9849. this.frame = document.createElement("div");
  9850. var styleAttr = this.frame.style;
  9851. styleAttr.position = "absolute";
  9852. styleAttr.visibility = "hidden";
  9853. styleAttr.border = "1px solid " + style.color.border;
  9854. styleAttr.color = style.fontColor;
  9855. styleAttr.fontSize = style.fontSize + "px";
  9856. styleAttr.fontFamily = style.fontFace;
  9857. styleAttr.padding = this.padding + "px";
  9858. styleAttr.backgroundColor = style.color.background;
  9859. styleAttr.borderRadius = "3px";
  9860. styleAttr.MozBorderRadius = "3px";
  9861. styleAttr.WebkitBorderRadius = "3px";
  9862. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  9863. styleAttr.whiteSpace = "nowrap";
  9864. this.container.appendChild(this.frame);
  9865. }
  9866. /**
  9867. * @param {number} x Horizontal position of the popup window
  9868. * @param {number} y Vertical position of the popup window
  9869. */
  9870. Popup.prototype.setPosition = function(x, y) {
  9871. this.x = parseInt(x);
  9872. this.y = parseInt(y);
  9873. };
  9874. /**
  9875. * Set the text for the popup window. This can be HTML code
  9876. * @param {string} text
  9877. */
  9878. Popup.prototype.setText = function(text) {
  9879. this.frame.innerHTML = text;
  9880. };
  9881. /**
  9882. * Show the popup window
  9883. * @param {boolean} show Optional. Show or hide the window
  9884. */
  9885. Popup.prototype.show = function (show) {
  9886. if (show === undefined) {
  9887. show = true;
  9888. }
  9889. if (show) {
  9890. var height = this.frame.clientHeight;
  9891. var width = this.frame.clientWidth;
  9892. var maxHeight = this.frame.parentNode.clientHeight;
  9893. var maxWidth = this.frame.parentNode.clientWidth;
  9894. var top = (this.y - height);
  9895. if (top + height + this.padding > maxHeight) {
  9896. top = maxHeight - height - this.padding;
  9897. }
  9898. if (top < this.padding) {
  9899. top = this.padding;
  9900. }
  9901. var left = this.x;
  9902. if (left + width + this.padding > maxWidth) {
  9903. left = maxWidth - width - this.padding;
  9904. }
  9905. if (left < this.padding) {
  9906. left = this.padding;
  9907. }
  9908. this.frame.style.left = left + "px";
  9909. this.frame.style.top = top + "px";
  9910. this.frame.style.visibility = "visible";
  9911. }
  9912. else {
  9913. this.hide();
  9914. }
  9915. };
  9916. /**
  9917. * Hide the popup window
  9918. */
  9919. Popup.prototype.hide = function () {
  9920. this.frame.style.visibility = "hidden";
  9921. };
  9922. /**
  9923. * @class Groups
  9924. * This class can store groups and properties specific for groups.
  9925. */
  9926. Groups = function () {
  9927. this.clear();
  9928. this.defaultIndex = 0;
  9929. };
  9930. /**
  9931. * default constants for group colors
  9932. */
  9933. Groups.DEFAULT = [
  9934. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  9935. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  9936. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  9937. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  9938. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  9939. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  9940. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  9941. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  9942. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  9943. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  9944. ];
  9945. /**
  9946. * Clear all groups
  9947. */
  9948. Groups.prototype.clear = function () {
  9949. this.groups = {};
  9950. this.groups.length = function()
  9951. {
  9952. var i = 0;
  9953. for ( var p in this ) {
  9954. if (this.hasOwnProperty(p)) {
  9955. i++;
  9956. }
  9957. }
  9958. return i;
  9959. }
  9960. };
  9961. /**
  9962. * get group properties of a groupname. If groupname is not found, a new group
  9963. * is added.
  9964. * @param {*} groupname Can be a number, string, Date, etc.
  9965. * @return {Object} group The created group, containing all group properties
  9966. */
  9967. Groups.prototype.get = function (groupname) {
  9968. var group = this.groups[groupname];
  9969. if (group == undefined) {
  9970. // create new group
  9971. var index = this.defaultIndex % Groups.DEFAULT.length;
  9972. this.defaultIndex++;
  9973. group = {};
  9974. group.color = Groups.DEFAULT[index];
  9975. this.groups[groupname] = group;
  9976. }
  9977. return group;
  9978. };
  9979. /**
  9980. * Add a custom group style
  9981. * @param {String} groupname
  9982. * @param {Object} style An object containing borderColor,
  9983. * backgroundColor, etc.
  9984. * @return {Object} group The created group object
  9985. */
  9986. Groups.prototype.add = function (groupname, style) {
  9987. this.groups[groupname] = style;
  9988. if (style.color) {
  9989. style.color = util.parseColor(style.color);
  9990. }
  9991. return style;
  9992. };
  9993. /**
  9994. * @class Images
  9995. * This class loads images and keeps them stored.
  9996. */
  9997. Images = function () {
  9998. this.images = {};
  9999. this.callback = undefined;
  10000. };
  10001. /**
  10002. * Set an onload callback function. This will be called each time an image
  10003. * is loaded
  10004. * @param {function} callback
  10005. */
  10006. Images.prototype.setOnloadCallback = function(callback) {
  10007. this.callback = callback;
  10008. };
  10009. /**
  10010. *
  10011. * @param {string} url Url of the image
  10012. * @return {Image} img The image object
  10013. */
  10014. Images.prototype.load = function(url) {
  10015. var img = this.images[url];
  10016. if (img == undefined) {
  10017. // create the image
  10018. var images = this;
  10019. img = new Image();
  10020. this.images[url] = img;
  10021. img.onload = function() {
  10022. if (images.callback) {
  10023. images.callback(this);
  10024. }
  10025. };
  10026. img.src = url;
  10027. }
  10028. return img;
  10029. };
  10030. /**
  10031. * Created by Alex on 2/6/14.
  10032. */
  10033. var physicsMixin = {
  10034. /**
  10035. * Toggling barnes Hut calculation on and off.
  10036. *
  10037. * @private
  10038. */
  10039. _toggleBarnesHut: function () {
  10040. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  10041. this._loadSelectedForceSolver();
  10042. this.moving = true;
  10043. this.start();
  10044. },
  10045. /**
  10046. * This loads the node force solver based on the barnes hut or repulsion algorithm
  10047. *
  10048. * @private
  10049. */
  10050. _loadSelectedForceSolver: function () {
  10051. // this overloads the this._calculateNodeForces
  10052. if (this.constants.physics.barnesHut.enabled == true) {
  10053. this._clearMixin(repulsionMixin);
  10054. this._clearMixin(hierarchalRepulsionMixin);
  10055. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  10056. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  10057. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  10058. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  10059. this._loadMixin(barnesHutMixin);
  10060. }
  10061. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  10062. this._clearMixin(barnesHutMixin);
  10063. this._clearMixin(repulsionMixin);
  10064. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  10065. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  10066. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  10067. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  10068. this._loadMixin(hierarchalRepulsionMixin);
  10069. }
  10070. else {
  10071. this._clearMixin(barnesHutMixin);
  10072. this._clearMixin(hierarchalRepulsionMixin);
  10073. this.barnesHutTree = undefined;
  10074. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  10075. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  10076. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  10077. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  10078. this._loadMixin(repulsionMixin);
  10079. }
  10080. },
  10081. /**
  10082. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  10083. * if there is more than one node. If it is just one node, we dont calculate anything.
  10084. *
  10085. * @private
  10086. */
  10087. _initializeForceCalculation: function () {
  10088. // stop calculation if there is only one node
  10089. if (this.nodeIndices.length == 1) {
  10090. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  10091. }
  10092. else {
  10093. // if there are too many nodes on screen, we cluster without repositioning
  10094. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  10095. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  10096. }
  10097. // we now start the force calculation
  10098. this._calculateForces();
  10099. }
  10100. },
  10101. /**
  10102. * Calculate the external forces acting on the nodes
  10103. * Forces are caused by: edges, repulsing forces between nodes, gravity
  10104. * @private
  10105. */
  10106. _calculateForces: function () {
  10107. // Gravity is required to keep separated groups from floating off
  10108. // the forces are reset to zero in this loop by using _setForce instead
  10109. // of _addForce
  10110. this._calculateGravitationalForces();
  10111. this._calculateNodeForces();
  10112. if (this.constants.smoothCurves == true) {
  10113. this._calculateSpringForcesWithSupport();
  10114. }
  10115. else {
  10116. this._calculateSpringForces();
  10117. }
  10118. },
  10119. /**
  10120. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  10121. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  10122. * This function joins the datanodes and invisible (called support) nodes into one object.
  10123. * We do this so we do not contaminate this.nodes with the support nodes.
  10124. *
  10125. * @private
  10126. */
  10127. _updateCalculationNodes: function () {
  10128. if (this.constants.smoothCurves == true) {
  10129. this.calculationNodes = {};
  10130. this.calculationNodeIndices = [];
  10131. for (var nodeId in this.nodes) {
  10132. if (this.nodes.hasOwnProperty(nodeId)) {
  10133. this.calculationNodes[nodeId] = this.nodes[nodeId];
  10134. }
  10135. }
  10136. var supportNodes = this.sectors['support']['nodes'];
  10137. for (var supportNodeId in supportNodes) {
  10138. if (supportNodes.hasOwnProperty(supportNodeId)) {
  10139. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  10140. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  10141. }
  10142. else {
  10143. supportNodes[supportNodeId]._setForce(0, 0);
  10144. }
  10145. }
  10146. }
  10147. for (var idx in this.calculationNodes) {
  10148. if (this.calculationNodes.hasOwnProperty(idx)) {
  10149. this.calculationNodeIndices.push(idx);
  10150. }
  10151. }
  10152. }
  10153. else {
  10154. this.calculationNodes = this.nodes;
  10155. this.calculationNodeIndices = this.nodeIndices;
  10156. }
  10157. },
  10158. /**
  10159. * this function applies the central gravity effect to keep groups from floating off
  10160. *
  10161. * @private
  10162. */
  10163. _calculateGravitationalForces: function () {
  10164. var dx, dy, distance, node, i;
  10165. var nodes = this.calculationNodes;
  10166. var gravity = this.constants.physics.centralGravity;
  10167. var gravityForce = 0;
  10168. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  10169. node = nodes[this.calculationNodeIndices[i]];
  10170. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  10171. // gravity does not apply when we are in a pocket sector
  10172. if (this._sector() == "default" && gravity != 0) {
  10173. dx = -node.x;
  10174. dy = -node.y;
  10175. distance = Math.sqrt(dx * dx + dy * dy);
  10176. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  10177. node.fx = dx * gravityForce;
  10178. node.fy = dy * gravityForce;
  10179. }
  10180. else {
  10181. node.fx = 0;
  10182. node.fy = 0;
  10183. }
  10184. }
  10185. },
  10186. /**
  10187. * this function calculates the effects of the springs in the case of unsmooth curves.
  10188. *
  10189. * @private
  10190. */
  10191. _calculateSpringForces: function () {
  10192. var edgeLength, edge, edgeId;
  10193. var dx, dy, fx, fy, springForce, length;
  10194. var edges = this.edges;
  10195. // forces caused by the edges, modelled as springs
  10196. for (edgeId in edges) {
  10197. if (edges.hasOwnProperty(edgeId)) {
  10198. edge = edges[edgeId];
  10199. if (edge.connected) {
  10200. // only calculate forces if nodes are in the same sector
  10201. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  10202. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  10203. // this implies that the edges between big clusters are longer
  10204. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  10205. dx = (edge.from.x - edge.to.x);
  10206. dy = (edge.from.y - edge.to.y);
  10207. length = Math.sqrt(dx * dx + dy * dy);
  10208. if (length == 0) {
  10209. length = 0.01;
  10210. }
  10211. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  10212. fx = dx * springForce;
  10213. fy = dy * springForce;
  10214. edge.from.fx += fx;
  10215. edge.from.fy += fy;
  10216. edge.to.fx -= fx;
  10217. edge.to.fy -= fy;
  10218. }
  10219. }
  10220. }
  10221. }
  10222. },
  10223. /**
  10224. * This function calculates the springforces on the nodes, accounting for the support nodes.
  10225. *
  10226. * @private
  10227. */
  10228. _calculateSpringForcesWithSupport: function () {
  10229. var edgeLength, edge, edgeId, combinedClusterSize;
  10230. var edges = this.edges;
  10231. // forces caused by the edges, modelled as springs
  10232. for (edgeId in edges) {
  10233. if (edges.hasOwnProperty(edgeId)) {
  10234. edge = edges[edgeId];
  10235. if (edge.connected) {
  10236. // only calculate forces if nodes are in the same sector
  10237. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  10238. if (edge.via != null) {
  10239. var node1 = edge.to;
  10240. var node2 = edge.via;
  10241. var node3 = edge.from;
  10242. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  10243. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  10244. // this implies that the edges between big clusters are longer
  10245. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  10246. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  10247. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  10248. }
  10249. }
  10250. }
  10251. }
  10252. }
  10253. },
  10254. /**
  10255. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  10256. *
  10257. * @param node1
  10258. * @param node2
  10259. * @param edgeLength
  10260. * @private
  10261. */
  10262. _calculateSpringForce: function (node1, node2, edgeLength) {
  10263. var dx, dy, fx, fy, springForce, length;
  10264. dx = (node1.x - node2.x);
  10265. dy = (node1.y - node2.y);
  10266. length = Math.sqrt(dx * dx + dy * dy);
  10267. if (length == 0) {
  10268. length = 0.01;
  10269. }
  10270. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  10271. fx = dx * springForce;
  10272. fy = dy * springForce;
  10273. node1.fx += fx;
  10274. node1.fy += fy;
  10275. node2.fx -= fx;
  10276. node2.fy -= fy;
  10277. },
  10278. /**
  10279. * Load the HTML for the physics config and bind it
  10280. * @private
  10281. */
  10282. _loadPhysicsConfiguration: function () {
  10283. if (this.physicsConfiguration === undefined) {
  10284. this.backupConstants = {};
  10285. util.copyObject(this.constants, this.backupConstants);
  10286. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  10287. this.physicsConfiguration = document.createElement('div');
  10288. this.physicsConfiguration.className = "PhysicsConfiguration";
  10289. this.physicsConfiguration.innerHTML = '' +
  10290. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  10291. '<tr>' +
  10292. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  10293. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  10294. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  10295. '</tr>' +
  10296. '</table>' +
  10297. '<table id="graph_BH_table" style="display:none">' +
  10298. '<tr><td><b>Barnes Hut</b></td></tr>' +
  10299. '<tr>' +
  10300. '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="500" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
  10301. '</tr>' +
  10302. '<tr>' +
  10303. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' +
  10304. '</tr>' +
  10305. '<tr>' +
  10306. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' +
  10307. '</tr>' +
  10308. '<tr>' +
  10309. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.001" style="width:300px" id="graph_BH_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' +
  10310. '</tr>' +
  10311. '<tr>' +
  10312. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' +
  10313. '</tr>' +
  10314. '</table>' +
  10315. '<table id="graph_R_table" style="display:none">' +
  10316. '<tr><td><b>Repulsion</b></td></tr>' +
  10317. '<tr>' +
  10318. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' +
  10319. '</tr>' +
  10320. '<tr>' +
  10321. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' +
  10322. '</tr>' +
  10323. '<tr>' +
  10324. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' +
  10325. '</tr>' +
  10326. '<tr>' +
  10327. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' +
  10328. '</tr>' +
  10329. '<tr>' +
  10330. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' +
  10331. '</tr>' +
  10332. '</table>' +
  10333. '<table id="graph_H_table" style="display:none">' +
  10334. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  10335. '<tr>' +
  10336. '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' +
  10337. '</tr>' +
  10338. '<tr>' +
  10339. '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' +
  10340. '</tr>' +
  10341. '<tr>' +
  10342. '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' +
  10343. '</tr>' +
  10344. '<tr>' +
  10345. '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' +
  10346. '</tr>' +
  10347. '<tr>' +
  10348. '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' +
  10349. '</tr>' +
  10350. '<tr>' +
  10351. '<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' +
  10352. '</tr>' +
  10353. '<tr>' +
  10354. '<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' +
  10355. '</tr>' +
  10356. '<tr>' +
  10357. '<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' +
  10358. '</tr>' +
  10359. '</table>' +
  10360. '<table><tr><td><b>Options:</b></td></tr>' +
  10361. '<tr>' +
  10362. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  10363. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  10364. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  10365. '</tr>' +
  10366. '</table>'
  10367. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  10368. this.optionsDiv = document.createElement("div");
  10369. this.optionsDiv.style.fontSize = "14px";
  10370. this.optionsDiv.style.fontFamily = "verdana";
  10371. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  10372. var rangeElement;
  10373. rangeElement = document.getElementById('graph_BH_gc');
  10374. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  10375. rangeElement = document.getElementById('graph_BH_cg');
  10376. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  10377. rangeElement = document.getElementById('graph_BH_sc');
  10378. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  10379. rangeElement = document.getElementById('graph_BH_sl');
  10380. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  10381. rangeElement = document.getElementById('graph_BH_damp');
  10382. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  10383. rangeElement = document.getElementById('graph_R_nd');
  10384. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  10385. rangeElement = document.getElementById('graph_R_cg');
  10386. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  10387. rangeElement = document.getElementById('graph_R_sc');
  10388. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  10389. rangeElement = document.getElementById('graph_R_sl');
  10390. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  10391. rangeElement = document.getElementById('graph_R_damp');
  10392. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  10393. rangeElement = document.getElementById('graph_H_nd');
  10394. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  10395. rangeElement = document.getElementById('graph_H_cg');
  10396. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  10397. rangeElement = document.getElementById('graph_H_sc');
  10398. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  10399. rangeElement = document.getElementById('graph_H_sl');
  10400. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  10401. rangeElement = document.getElementById('graph_H_damp');
  10402. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  10403. rangeElement = document.getElementById('graph_H_direction');
  10404. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  10405. rangeElement = document.getElementById('graph_H_levsep');
  10406. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  10407. rangeElement = document.getElementById('graph_H_nspac');
  10408. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  10409. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10410. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10411. var radioButton3 = document.getElementById("graph_physicsMethod3");
  10412. radioButton2.checked = true;
  10413. if (this.constants.physics.barnesHut.enabled) {
  10414. radioButton1.checked = true;
  10415. }
  10416. if (this.constants.hierarchicalLayout.enabled) {
  10417. radioButton3.checked = true;
  10418. }
  10419. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10420. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  10421. var graph_generateOptions = document.getElementById("graph_generateOptions");
  10422. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  10423. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  10424. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  10425. if (this.constants.smoothCurves == true) {
  10426. graph_toggleSmooth.style.background = "#A4FF56";
  10427. }
  10428. else {
  10429. graph_toggleSmooth.style.background = "#FF8532";
  10430. }
  10431. switchConfigurations.apply(this);
  10432. radioButton1.onchange = switchConfigurations.bind(this);
  10433. radioButton2.onchange = switchConfigurations.bind(this);
  10434. radioButton3.onchange = switchConfigurations.bind(this);
  10435. }
  10436. },
  10437. _overWriteGraphConstants: function (constantsVariableName, value) {
  10438. var nameArray = constantsVariableName.split("_");
  10439. if (nameArray.length == 1) {
  10440. this.constants[nameArray[0]] = value;
  10441. }
  10442. else if (nameArray.length == 2) {
  10443. this.constants[nameArray[0]][nameArray[1]] = value;
  10444. }
  10445. else if (nameArray.length == 3) {
  10446. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  10447. }
  10448. }
  10449. };
  10450. function graphToggleSmoothCurves () {
  10451. this.constants.smoothCurves = !this.constants.smoothCurves;
  10452. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10453. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  10454. else {graph_toggleSmooth.style.background = "#FF8532";}
  10455. this._configureSmoothCurves(false);
  10456. };
  10457. function graphRepositionNodes () {
  10458. for (var nodeId in this.calculationNodes) {
  10459. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  10460. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  10461. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  10462. }
  10463. }
  10464. if (this.constants.hierarchicalLayout.enabled == true) {
  10465. this._setupHierarchicalLayout();
  10466. }
  10467. else {
  10468. this.repositionNodes();
  10469. }
  10470. this.moving = true;
  10471. this.start();
  10472. };
  10473. function graphGenerateOptions () {
  10474. var options = "No options are required, default values used.";
  10475. var optionsSpecific = [];
  10476. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10477. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10478. if (radioButton1.checked == true) {
  10479. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  10480. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10481. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10482. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10483. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10484. if (optionsSpecific.length != 0) {
  10485. options = "var options = {";
  10486. options += "physics: {barnesHut: {";
  10487. for (var i = 0; i < optionsSpecific.length; i++) {
  10488. options += optionsSpecific[i];
  10489. if (i < optionsSpecific.length - 1) {
  10490. options += ", "
  10491. }
  10492. }
  10493. options += '}}'
  10494. }
  10495. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10496. if (optionsSpecific.length == 0) {options = "var options = {";}
  10497. else {options += ", "}
  10498. options += "smoothCurves: " + this.constants.smoothCurves;
  10499. }
  10500. if (options != "No options are required, default values used.") {
  10501. options += '};'
  10502. }
  10503. }
  10504. else if (radioButton2.checked == true) {
  10505. options = "var options = {";
  10506. options += "physics: {barnesHut: {enabled: false}";
  10507. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  10508. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10509. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10510. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10511. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10512. if (optionsSpecific.length != 0) {
  10513. options += ", repulsion: {";
  10514. for (var i = 0; i < optionsSpecific.length; i++) {
  10515. options += optionsSpecific[i];
  10516. if (i < optionsSpecific.length - 1) {
  10517. options += ", "
  10518. }
  10519. }
  10520. options += '}}'
  10521. }
  10522. if (optionsSpecific.length == 0) {options += "}"}
  10523. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10524. options += ", smoothCurves: " + this.constants.smoothCurves;
  10525. }
  10526. options += '};'
  10527. }
  10528. else {
  10529. options = "var options = {";
  10530. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  10531. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10532. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10533. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10534. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10535. if (optionsSpecific.length != 0) {
  10536. options += "physics: {hierarchicalRepulsion: {";
  10537. for (var i = 0; i < optionsSpecific.length; i++) {
  10538. options += optionsSpecific[i];
  10539. if (i < optionsSpecific.length - 1) {
  10540. options += ", ";
  10541. }
  10542. }
  10543. options += '}},';
  10544. }
  10545. options += 'hierarchicalLayout: {';
  10546. optionsSpecific = [];
  10547. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  10548. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  10549. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  10550. if (optionsSpecific.length != 0) {
  10551. for (var i = 0; i < optionsSpecific.length; i++) {
  10552. options += optionsSpecific[i];
  10553. if (i < optionsSpecific.length - 1) {
  10554. options += ", "
  10555. }
  10556. }
  10557. options += '}'
  10558. }
  10559. else {
  10560. options += "enabled:true}";
  10561. }
  10562. options += '};'
  10563. }
  10564. this.optionsDiv.innerHTML = options;
  10565. };
  10566. function switchConfigurations () {
  10567. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  10568. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  10569. var tableId = "graph_" + radioButton + "_table";
  10570. var table = document.getElementById(tableId);
  10571. table.style.display = "block";
  10572. for (var i = 0; i < ids.length; i++) {
  10573. if (ids[i] != tableId) {
  10574. table = document.getElementById(ids[i]);
  10575. table.style.display = "none";
  10576. }
  10577. }
  10578. this._restoreNodes();
  10579. if (radioButton == "R") {
  10580. this.constants.hierarchicalLayout.enabled = false;
  10581. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10582. this.constants.physics.barnesHut.enabled = false;
  10583. }
  10584. else if (radioButton == "H") {
  10585. this.constants.hierarchicalLayout.enabled = true;
  10586. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10587. this.constants.physics.barnesHut.enabled = false;
  10588. this._setupHierarchicalLayout();
  10589. }
  10590. else {
  10591. this.constants.hierarchicalLayout.enabled = false;
  10592. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10593. this.constants.physics.barnesHut.enabled = true;
  10594. }
  10595. this._loadSelectedForceSolver();
  10596. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10597. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  10598. else {graph_toggleSmooth.style.background = "#FF8532";}
  10599. this.moving = true;
  10600. this.start();
  10601. }
  10602. function showValueOfRange (id,map,constantsVariableName) {
  10603. var valueId = id + "_value";
  10604. var rangeValue = document.getElementById(id).value;
  10605. if (map instanceof Array) {
  10606. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  10607. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  10608. }
  10609. else {
  10610. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  10611. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  10612. }
  10613. if (constantsVariableName == "hierarchicalLayout_direction" ||
  10614. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  10615. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  10616. this._setupHierarchicalLayout();
  10617. }
  10618. this.moving = true;
  10619. this.start();
  10620. };
  10621. /**
  10622. * Created by Alex on 2/10/14.
  10623. */
  10624. var hierarchalRepulsionMixin = {
  10625. /**
  10626. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10627. * This field is linearly approximated.
  10628. *
  10629. * @private
  10630. */
  10631. _calculateNodeForces: function () {
  10632. var dx, dy, distance, fx, fy, combinedClusterSize,
  10633. repulsingForce, node1, node2, i, j;
  10634. var nodes = this.calculationNodes;
  10635. var nodeIndices = this.calculationNodeIndices;
  10636. // approximation constants
  10637. var b = 5;
  10638. var a_base = 0.5 * -b;
  10639. // repulsing forces between nodes
  10640. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  10641. var minimumDistance = nodeDistance;
  10642. // we loop from i over all but the last entree in the array
  10643. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10644. for (i = 0; i < nodeIndices.length - 1; i++) {
  10645. node1 = nodes[nodeIndices[i]];
  10646. for (j = i + 1; j < nodeIndices.length; j++) {
  10647. node2 = nodes[nodeIndices[j]];
  10648. dx = node2.x - node1.x;
  10649. dy = node2.y - node1.y;
  10650. distance = Math.sqrt(dx * dx + dy * dy);
  10651. var a = a_base / minimumDistance;
  10652. if (distance < 2 * minimumDistance) {
  10653. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10654. // normalize force with
  10655. if (distance == 0) {
  10656. distance = 0.01;
  10657. }
  10658. else {
  10659. repulsingForce = repulsingForce / distance;
  10660. }
  10661. fx = dx * repulsingForce;
  10662. fy = dy * repulsingForce;
  10663. node1.fx -= fx;
  10664. node1.fy -= fy;
  10665. node2.fx += fx;
  10666. node2.fy += fy;
  10667. }
  10668. }
  10669. }
  10670. }
  10671. };
  10672. /**
  10673. * Created by Alex on 2/10/14.
  10674. */
  10675. var barnesHutMixin = {
  10676. /**
  10677. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  10678. * The Barnes Hut method is used to speed up this N-body simulation.
  10679. *
  10680. * @private
  10681. */
  10682. _calculateNodeForces : function() {
  10683. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  10684. var node;
  10685. var nodes = this.calculationNodes;
  10686. var nodeIndices = this.calculationNodeIndices;
  10687. var nodeCount = nodeIndices.length;
  10688. this._formBarnesHutTree(nodes,nodeIndices);
  10689. var barnesHutTree = this.barnesHutTree;
  10690. // place the nodes one by one recursively
  10691. for (var i = 0; i < nodeCount; i++) {
  10692. node = nodes[nodeIndices[i]];
  10693. // starting with root is irrelevant, it never passes the BarnesHut condition
  10694. this._getForceContribution(barnesHutTree.root.children.NW,node);
  10695. this._getForceContribution(barnesHutTree.root.children.NE,node);
  10696. this._getForceContribution(barnesHutTree.root.children.SW,node);
  10697. this._getForceContribution(barnesHutTree.root.children.SE,node);
  10698. }
  10699. }
  10700. },
  10701. /**
  10702. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  10703. * If a region contains a single node, we check if it is not itself, then we apply the force.
  10704. *
  10705. * @param parentBranch
  10706. * @param node
  10707. * @private
  10708. */
  10709. _getForceContribution : function(parentBranch,node) {
  10710. // we get no force contribution from an empty region
  10711. if (parentBranch.childrenCount > 0) {
  10712. var dx,dy,distance;
  10713. // get the distance from the center of mass to the node.
  10714. dx = parentBranch.centerOfMass.x - node.x;
  10715. dy = parentBranch.centerOfMass.y - node.y;
  10716. distance = Math.sqrt(dx * dx + dy * dy);
  10717. // BarnesHut condition
  10718. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  10719. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  10720. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  10721. // duplicate code to reduce function calls to speed up program
  10722. if (distance == 0) {
  10723. distance = 0.1*Math.random();
  10724. dx = distance;
  10725. }
  10726. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10727. var fx = dx * gravityForce;
  10728. var fy = dy * gravityForce;
  10729. node.fx += fx;
  10730. node.fy += fy;
  10731. }
  10732. else {
  10733. // Did not pass the condition, go into children if available
  10734. if (parentBranch.childrenCount == 4) {
  10735. this._getForceContribution(parentBranch.children.NW,node);
  10736. this._getForceContribution(parentBranch.children.NE,node);
  10737. this._getForceContribution(parentBranch.children.SW,node);
  10738. this._getForceContribution(parentBranch.children.SE,node);
  10739. }
  10740. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  10741. if (parentBranch.children.data.id != node.id) { // if it is not self
  10742. // duplicate code to reduce function calls to speed up program
  10743. if (distance == 0) {
  10744. distance = 0.5*Math.random();
  10745. dx = distance;
  10746. }
  10747. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10748. var fx = dx * gravityForce;
  10749. var fy = dy * gravityForce;
  10750. node.fx += fx;
  10751. node.fy += fy;
  10752. }
  10753. }
  10754. }
  10755. }
  10756. },
  10757. /**
  10758. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  10759. *
  10760. * @param nodes
  10761. * @param nodeIndices
  10762. * @private
  10763. */
  10764. _formBarnesHutTree : function(nodes,nodeIndices) {
  10765. var node;
  10766. var nodeCount = nodeIndices.length;
  10767. var minX = Number.MAX_VALUE,
  10768. minY = Number.MAX_VALUE,
  10769. maxX =-Number.MAX_VALUE,
  10770. maxY =-Number.MAX_VALUE;
  10771. // get the range of the nodes
  10772. for (var i = 0; i < nodeCount; i++) {
  10773. var x = nodes[nodeIndices[i]].x;
  10774. var y = nodes[nodeIndices[i]].y;
  10775. if (x < minX) { minX = x; }
  10776. if (x > maxX) { maxX = x; }
  10777. if (y < minY) { minY = y; }
  10778. if (y > maxY) { maxY = y; }
  10779. }
  10780. // make the range a square
  10781. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  10782. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  10783. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  10784. var minimumTreeSize = 1e-5;
  10785. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  10786. var halfRootSize = 0.5 * rootSize;
  10787. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  10788. // construct the barnesHutTree
  10789. var barnesHutTree = {root:{
  10790. centerOfMass:{x:0,y:0}, // Center of Mass
  10791. mass:0,
  10792. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  10793. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  10794. size: rootSize,
  10795. calcSize: 1 / rootSize,
  10796. children: {data:null},
  10797. maxWidth: 0,
  10798. level: 0,
  10799. childrenCount: 4
  10800. }};
  10801. this._splitBranch(barnesHutTree.root);
  10802. // place the nodes one by one recursively
  10803. for (i = 0; i < nodeCount; i++) {
  10804. node = nodes[nodeIndices[i]];
  10805. this._placeInTree(barnesHutTree.root,node);
  10806. }
  10807. // make global
  10808. this.barnesHutTree = barnesHutTree
  10809. },
  10810. _updateBranchMass : function(parentBranch, node) {
  10811. var totalMass = parentBranch.mass + node.mass;
  10812. var totalMassInv = 1/totalMass;
  10813. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  10814. parentBranch.centerOfMass.x *= totalMassInv;
  10815. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  10816. parentBranch.centerOfMass.y *= totalMassInv;
  10817. parentBranch.mass = totalMass;
  10818. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  10819. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  10820. },
  10821. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  10822. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  10823. // update the mass of the branch.
  10824. this._updateBranchMass(parentBranch,node);
  10825. }
  10826. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  10827. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  10828. this._placeInRegion(parentBranch,node,"NW");
  10829. }
  10830. else { // in SW
  10831. this._placeInRegion(parentBranch,node,"SW");
  10832. }
  10833. }
  10834. else { // in NE or SE
  10835. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  10836. this._placeInRegion(parentBranch,node,"NE");
  10837. }
  10838. else { // in SE
  10839. this._placeInRegion(parentBranch,node,"SE");
  10840. }
  10841. }
  10842. },
  10843. _placeInRegion : function(parentBranch,node,region) {
  10844. switch (parentBranch.children[region].childrenCount) {
  10845. case 0: // place node here
  10846. parentBranch.children[region].children.data = node;
  10847. parentBranch.children[region].childrenCount = 1;
  10848. this._updateBranchMass(parentBranch.children[region],node);
  10849. break;
  10850. case 1: // convert into children
  10851. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  10852. // we move one node a pixel and we do not put it in the tree.
  10853. if (parentBranch.children[region].children.data.x == node.x &&
  10854. parentBranch.children[region].children.data.y == node.y) {
  10855. node.x += Math.random();
  10856. node.y += Math.random();
  10857. }
  10858. else {
  10859. this._splitBranch(parentBranch.children[region]);
  10860. this._placeInTree(parentBranch.children[region],node);
  10861. }
  10862. break;
  10863. case 4: // place in branch
  10864. this._placeInTree(parentBranch.children[region],node);
  10865. break;
  10866. }
  10867. },
  10868. /**
  10869. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  10870. * after the split is complete.
  10871. *
  10872. * @param parentBranch
  10873. * @private
  10874. */
  10875. _splitBranch : function(parentBranch) {
  10876. // if the branch is filled with a node, replace the node in the new subset.
  10877. var containedNode = null;
  10878. if (parentBranch.childrenCount == 1) {
  10879. containedNode = parentBranch.children.data;
  10880. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  10881. }
  10882. parentBranch.childrenCount = 4;
  10883. parentBranch.children.data = null;
  10884. this._insertRegion(parentBranch,"NW");
  10885. this._insertRegion(parentBranch,"NE");
  10886. this._insertRegion(parentBranch,"SW");
  10887. this._insertRegion(parentBranch,"SE");
  10888. if (containedNode != null) {
  10889. this._placeInTree(parentBranch,containedNode);
  10890. }
  10891. },
  10892. /**
  10893. * This function subdivides the region into four new segments.
  10894. * Specifically, this inserts a single new segment.
  10895. * It fills the children section of the parentBranch
  10896. *
  10897. * @param parentBranch
  10898. * @param region
  10899. * @param parentRange
  10900. * @private
  10901. */
  10902. _insertRegion : function(parentBranch, region) {
  10903. var minX,maxX,minY,maxY;
  10904. var childSize = 0.5 * parentBranch.size;
  10905. switch (region) {
  10906. case "NW":
  10907. minX = parentBranch.range.minX;
  10908. maxX = parentBranch.range.minX + childSize;
  10909. minY = parentBranch.range.minY;
  10910. maxY = parentBranch.range.minY + childSize;
  10911. break;
  10912. case "NE":
  10913. minX = parentBranch.range.minX + childSize;
  10914. maxX = parentBranch.range.maxX;
  10915. minY = parentBranch.range.minY;
  10916. maxY = parentBranch.range.minY + childSize;
  10917. break;
  10918. case "SW":
  10919. minX = parentBranch.range.minX;
  10920. maxX = parentBranch.range.minX + childSize;
  10921. minY = parentBranch.range.minY + childSize;
  10922. maxY = parentBranch.range.maxY;
  10923. break;
  10924. case "SE":
  10925. minX = parentBranch.range.minX + childSize;
  10926. maxX = parentBranch.range.maxX;
  10927. minY = parentBranch.range.minY + childSize;
  10928. maxY = parentBranch.range.maxY;
  10929. break;
  10930. }
  10931. parentBranch.children[region] = {
  10932. centerOfMass:{x:0,y:0},
  10933. mass:0,
  10934. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  10935. size: 0.5 * parentBranch.size,
  10936. calcSize: 2 * parentBranch.calcSize,
  10937. children: {data:null},
  10938. maxWidth: 0,
  10939. level: parentBranch.level+1,
  10940. childrenCount: 0
  10941. };
  10942. },
  10943. /**
  10944. * This function is for debugging purposed, it draws the tree.
  10945. *
  10946. * @param ctx
  10947. * @param color
  10948. * @private
  10949. */
  10950. _drawTree : function(ctx,color) {
  10951. if (this.barnesHutTree !== undefined) {
  10952. ctx.lineWidth = 1;
  10953. this._drawBranch(this.barnesHutTree.root,ctx,color);
  10954. }
  10955. },
  10956. /**
  10957. * This function is for debugging purposes. It draws the branches recursively.
  10958. *
  10959. * @param branch
  10960. * @param ctx
  10961. * @param color
  10962. * @private
  10963. */
  10964. _drawBranch : function(branch,ctx,color) {
  10965. if (color === undefined) {
  10966. color = "#FF0000";
  10967. }
  10968. if (branch.childrenCount == 4) {
  10969. this._drawBranch(branch.children.NW,ctx);
  10970. this._drawBranch(branch.children.NE,ctx);
  10971. this._drawBranch(branch.children.SE,ctx);
  10972. this._drawBranch(branch.children.SW,ctx);
  10973. }
  10974. ctx.strokeStyle = color;
  10975. ctx.beginPath();
  10976. ctx.moveTo(branch.range.minX,branch.range.minY);
  10977. ctx.lineTo(branch.range.maxX,branch.range.minY);
  10978. ctx.stroke();
  10979. ctx.beginPath();
  10980. ctx.moveTo(branch.range.maxX,branch.range.minY);
  10981. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  10982. ctx.stroke();
  10983. ctx.beginPath();
  10984. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  10985. ctx.lineTo(branch.range.minX,branch.range.maxY);
  10986. ctx.stroke();
  10987. ctx.beginPath();
  10988. ctx.moveTo(branch.range.minX,branch.range.maxY);
  10989. ctx.lineTo(branch.range.minX,branch.range.minY);
  10990. ctx.stroke();
  10991. /*
  10992. if (branch.mass > 0) {
  10993. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  10994. ctx.stroke();
  10995. }
  10996. */
  10997. }
  10998. };
  10999. /**
  11000. * Created by Alex on 2/10/14.
  11001. */
  11002. var repulsionMixin = {
  11003. /**
  11004. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  11005. * This field is linearly approximated.
  11006. *
  11007. * @private
  11008. */
  11009. _calculateNodeForces: function () {
  11010. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  11011. repulsingForce, node1, node2, i, j;
  11012. var nodes = this.calculationNodes;
  11013. var nodeIndices = this.calculationNodeIndices;
  11014. // approximation constants
  11015. var a_base = -2 / 3;
  11016. var b = 4 / 3;
  11017. // repulsing forces between nodes
  11018. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  11019. var minimumDistance = nodeDistance;
  11020. // we loop from i over all but the last entree in the array
  11021. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  11022. for (i = 0; i < nodeIndices.length - 1; i++) {
  11023. node1 = nodes[nodeIndices[i]];
  11024. for (j = i + 1; j < nodeIndices.length; j++) {
  11025. node2 = nodes[nodeIndices[j]];
  11026. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  11027. dx = node2.x - node1.x;
  11028. dy = node2.y - node1.y;
  11029. distance = Math.sqrt(dx * dx + dy * dy);
  11030. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  11031. var a = a_base / minimumDistance;
  11032. if (distance < 2 * minimumDistance) {
  11033. if (distance < 0.5 * minimumDistance) {
  11034. repulsingForce = 1.0;
  11035. }
  11036. else {
  11037. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  11038. }
  11039. // amplify the repulsion for clusters.
  11040. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  11041. repulsingForce = repulsingForce / distance;
  11042. fx = dx * repulsingForce;
  11043. fy = dy * repulsingForce;
  11044. node1.fx -= fx;
  11045. node1.fy -= fy;
  11046. node2.fx += fx;
  11047. node2.fy += fy;
  11048. }
  11049. }
  11050. }
  11051. }
  11052. };
  11053. var HierarchicalLayoutMixin = {
  11054. _resetLevels : function() {
  11055. for (var nodeId in this.nodes) {
  11056. if (this.nodes.hasOwnProperty(nodeId)) {
  11057. var node = this.nodes[nodeId];
  11058. if (node.preassignedLevel == false) {
  11059. node.level = -1;
  11060. }
  11061. }
  11062. }
  11063. },
  11064. /**
  11065. * This is the main function to layout the nodes in a hierarchical way.
  11066. * It checks if the node details are supplied correctly
  11067. *
  11068. * @private
  11069. */
  11070. _setupHierarchicalLayout : function() {
  11071. if (this.constants.hierarchicalLayout.enabled == true) {
  11072. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  11073. this.constants.hierarchicalLayout.levelSeparation *= -1;
  11074. }
  11075. else {
  11076. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  11077. }
  11078. // get the size of the largest hubs and check if the user has defined a level for a node.
  11079. var hubsize = 0;
  11080. var node, nodeId;
  11081. var definedLevel = false;
  11082. var undefinedLevel = false;
  11083. for (nodeId in this.nodes) {
  11084. if (this.nodes.hasOwnProperty(nodeId)) {
  11085. node = this.nodes[nodeId];
  11086. if (node.level != -1) {
  11087. definedLevel = true;
  11088. }
  11089. else {
  11090. undefinedLevel = true;
  11091. }
  11092. if (hubsize < node.edges.length) {
  11093. hubsize = node.edges.length;
  11094. }
  11095. }
  11096. }
  11097. // if the user defined some levels but not all, alert and run without hierarchical layout
  11098. if (undefinedLevel == true && definedLevel == true) {
  11099. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  11100. this.zoomExtent(true,this.constants.clustering.enabled);
  11101. if (!this.constants.clustering.enabled) {
  11102. this.start();
  11103. }
  11104. }
  11105. else {
  11106. // setup the system to use hierarchical method.
  11107. this._changeConstants();
  11108. // define levels if undefined by the users. Based on hubsize
  11109. if (undefinedLevel == true) {
  11110. this._determineLevels(hubsize);
  11111. }
  11112. // check the distribution of the nodes per level.
  11113. var distribution = this._getDistribution();
  11114. // place the nodes on the canvas. This also stablilizes the system.
  11115. this._placeNodesByHierarchy(distribution);
  11116. // start the simulation.
  11117. this.start();
  11118. }
  11119. }
  11120. },
  11121. /**
  11122. * This function places the nodes on the canvas based on the hierarchial distribution.
  11123. *
  11124. * @param {Object} distribution | obtained by the function this._getDistribution()
  11125. * @private
  11126. */
  11127. _placeNodesByHierarchy : function(distribution) {
  11128. var nodeId, node;
  11129. // start placing all the level 0 nodes first. Then recursively position their branches.
  11130. for (nodeId in distribution[0].nodes) {
  11131. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  11132. node = distribution[0].nodes[nodeId];
  11133. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11134. if (node.xFixed) {
  11135. node.x = distribution[0].minPos;
  11136. node.xFixed = false;
  11137. distribution[0].minPos += distribution[0].nodeSpacing;
  11138. }
  11139. }
  11140. else {
  11141. if (node.yFixed) {
  11142. node.y = distribution[0].minPos;
  11143. node.yFixed = false;
  11144. distribution[0].minPos += distribution[0].nodeSpacing;
  11145. }
  11146. }
  11147. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  11148. }
  11149. }
  11150. // stabilize the system after positioning. This function calls zoomExtent.
  11151. this._stabilize();
  11152. },
  11153. /**
  11154. * This function get the distribution of levels based on hubsize
  11155. *
  11156. * @returns {Object}
  11157. * @private
  11158. */
  11159. _getDistribution : function() {
  11160. var distribution = {};
  11161. var nodeId, node;
  11162. // 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.
  11163. // the fix of X is removed after the x value has been set.
  11164. for (nodeId in this.nodes) {
  11165. if (this.nodes.hasOwnProperty(nodeId)) {
  11166. node = this.nodes[nodeId];
  11167. node.xFixed = true;
  11168. node.yFixed = true;
  11169. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11170. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  11171. }
  11172. else {
  11173. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  11174. }
  11175. if (!distribution.hasOwnProperty(node.level)) {
  11176. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  11177. }
  11178. distribution[node.level].amount += 1;
  11179. distribution[node.level].nodes[node.id] = node;
  11180. }
  11181. }
  11182. // determine the largest amount of nodes of all levels
  11183. var maxCount = 0;
  11184. for (var level in distribution) {
  11185. if (distribution.hasOwnProperty(level)) {
  11186. if (maxCount < distribution[level].amount) {
  11187. maxCount = distribution[level].amount;
  11188. }
  11189. }
  11190. }
  11191. // set the initial position and spacing of each nodes accordingly
  11192. for (var level in distribution) {
  11193. if (distribution.hasOwnProperty(level)) {
  11194. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  11195. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  11196. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  11197. }
  11198. }
  11199. return distribution;
  11200. },
  11201. /**
  11202. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  11203. *
  11204. * @param hubsize
  11205. * @private
  11206. */
  11207. _determineLevels : function(hubsize) {
  11208. var nodeId, node;
  11209. // determine hubs
  11210. for (nodeId in this.nodes) {
  11211. if (this.nodes.hasOwnProperty(nodeId)) {
  11212. node = this.nodes[nodeId];
  11213. if (node.edges.length == hubsize) {
  11214. node.level = 0;
  11215. }
  11216. }
  11217. }
  11218. // branch from hubs
  11219. for (nodeId in this.nodes) {
  11220. if (this.nodes.hasOwnProperty(nodeId)) {
  11221. node = this.nodes[nodeId];
  11222. if (node.level == 0) {
  11223. this._setLevel(1,node.edges,node.id);
  11224. }
  11225. }
  11226. }
  11227. },
  11228. /**
  11229. * Since hierarchical layout does not support:
  11230. * - smooth curves (based on the physics),
  11231. * - clustering (based on dynamic node counts)
  11232. *
  11233. * We disable both features so there will be no problems.
  11234. *
  11235. * @private
  11236. */
  11237. _changeConstants : function() {
  11238. this.constants.clustering.enabled = false;
  11239. this.constants.physics.barnesHut.enabled = false;
  11240. this.constants.physics.hierarchicalRepulsion.enabled = true;
  11241. this._loadSelectedForceSolver();
  11242. this.constants.smoothCurves = false;
  11243. this._configureSmoothCurves();
  11244. },
  11245. /**
  11246. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  11247. * on a X position that ensures there will be no overlap.
  11248. *
  11249. * @param edges
  11250. * @param parentId
  11251. * @param distribution
  11252. * @param parentLevel
  11253. * @private
  11254. */
  11255. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  11256. for (var i = 0; i < edges.length; i++) {
  11257. var childNode = null;
  11258. if (edges[i].toId == parentId) {
  11259. childNode = edges[i].from;
  11260. }
  11261. else {
  11262. childNode = edges[i].to;
  11263. }
  11264. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  11265. var nodeMoved = false;
  11266. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  11267. if (childNode.xFixed && childNode.level > parentLevel) {
  11268. childNode.xFixed = false;
  11269. childNode.x = distribution[childNode.level].minPos;
  11270. nodeMoved = true;
  11271. }
  11272. }
  11273. else {
  11274. if (childNode.yFixed && childNode.level > parentLevel) {
  11275. childNode.yFixed = false;
  11276. childNode.y = distribution[childNode.level].minPos;
  11277. nodeMoved = true;
  11278. }
  11279. }
  11280. if (nodeMoved == true) {
  11281. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  11282. if (childNode.edges.length > 1) {
  11283. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  11284. }
  11285. }
  11286. }
  11287. },
  11288. /**
  11289. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  11290. *
  11291. * @param level
  11292. * @param edges
  11293. * @param parentId
  11294. * @private
  11295. */
  11296. _setLevel : function(level, edges, parentId) {
  11297. for (var i = 0; i < edges.length; i++) {
  11298. var childNode = null;
  11299. if (edges[i].toId == parentId) {
  11300. childNode = edges[i].from;
  11301. }
  11302. else {
  11303. childNode = edges[i].to;
  11304. }
  11305. if (childNode.level == -1 || childNode.level > level) {
  11306. childNode.level = level;
  11307. if (edges.length > 1) {
  11308. this._setLevel(level+1, childNode.edges, childNode.id);
  11309. }
  11310. }
  11311. }
  11312. },
  11313. /**
  11314. * Unfix nodes
  11315. *
  11316. * @private
  11317. */
  11318. _restoreNodes : function() {
  11319. for (nodeId in this.nodes) {
  11320. if (this.nodes.hasOwnProperty(nodeId)) {
  11321. this.nodes[nodeId].xFixed = false;
  11322. this.nodes[nodeId].yFixed = false;
  11323. }
  11324. }
  11325. }
  11326. };
  11327. /**
  11328. * Created by Alex on 2/4/14.
  11329. */
  11330. var manipulationMixin = {
  11331. /**
  11332. * clears the toolbar div element of children
  11333. *
  11334. * @private
  11335. */
  11336. _clearManipulatorBar : function() {
  11337. while (this.manipulationDiv.hasChildNodes()) {
  11338. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11339. }
  11340. },
  11341. /**
  11342. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  11343. * these functions to their original functionality, we saved them in this.cachedFunctions.
  11344. * This function restores these functions to their original function.
  11345. *
  11346. * @private
  11347. */
  11348. _restoreOverloadedFunctions : function() {
  11349. for (var functionName in this.cachedFunctions) {
  11350. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  11351. this[functionName] = this.cachedFunctions[functionName];
  11352. }
  11353. }
  11354. },
  11355. /**
  11356. * Enable or disable edit-mode.
  11357. *
  11358. * @private
  11359. */
  11360. _toggleEditMode : function() {
  11361. this.editMode = !this.editMode;
  11362. var toolbar = document.getElementById("graph-manipulationDiv");
  11363. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11364. var editModeDiv = document.getElementById("graph-manipulation-editMode");
  11365. if (this.editMode == true) {
  11366. toolbar.style.display="block";
  11367. closeDiv.style.display="block";
  11368. editModeDiv.style.display="none";
  11369. closeDiv.onclick = this._toggleEditMode.bind(this);
  11370. }
  11371. else {
  11372. toolbar.style.display="none";
  11373. closeDiv.style.display="none";
  11374. editModeDiv.style.display="block";
  11375. closeDiv.onclick = null;
  11376. }
  11377. this._createManipulatorBar()
  11378. },
  11379. /**
  11380. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  11381. *
  11382. * @private
  11383. */
  11384. _createManipulatorBar : function() {
  11385. // remove bound functions
  11386. if (this.boundFunction) {
  11387. this.off('select', this.boundFunction);
  11388. }
  11389. // restore overloaded functions
  11390. this._restoreOverloadedFunctions();
  11391. // resume calculation
  11392. this.freezeSimulation = false;
  11393. // reset global variables
  11394. this.blockConnectingEdgeSelection = false;
  11395. this.forceAppendSelection = false;
  11396. if (this.editMode == true) {
  11397. while (this.manipulationDiv.hasChildNodes()) {
  11398. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11399. }
  11400. // add the icons to the manipulator div
  11401. this.manipulationDiv.innerHTML = "" +
  11402. "<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
  11403. "<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  11404. "<div class='graph-seperatorLine'></div>" +
  11405. "<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
  11406. "<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  11407. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11408. this.manipulationDiv.innerHTML += "" +
  11409. "<div class='graph-seperatorLine'></div>" +
  11410. "<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
  11411. "<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  11412. }
  11413. if (this._selectionIsEmpty() == false) {
  11414. this.manipulationDiv.innerHTML += "" +
  11415. "<div class='graph-seperatorLine'></div>" +
  11416. "<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
  11417. "<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  11418. }
  11419. // bind the icons
  11420. var addNodeButton = document.getElementById("graph-manipulate-addNode");
  11421. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  11422. var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
  11423. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  11424. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11425. var editButton = document.getElementById("graph-manipulate-editNode");
  11426. editButton.onclick = this._editNode.bind(this);
  11427. }
  11428. if (this._selectionIsEmpty() == false) {
  11429. var deleteButton = document.getElementById("graph-manipulate-delete");
  11430. deleteButton.onclick = this._deleteSelected.bind(this);
  11431. }
  11432. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11433. closeDiv.onclick = this._toggleEditMode.bind(this);
  11434. this.boundFunction = this._createManipulatorBar.bind(this);
  11435. this.on('select', this.boundFunction);
  11436. }
  11437. else {
  11438. this.editModeDiv.innerHTML = "" +
  11439. "<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
  11440. "<span class='graph-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  11441. var editModeButton = document.getElementById("graph-manipulate-editModeButton");
  11442. editModeButton.onclick = this._toggleEditMode.bind(this);
  11443. }
  11444. },
  11445. /**
  11446. * Create the toolbar for adding Nodes
  11447. *
  11448. * @private
  11449. */
  11450. _createAddNodeToolbar : function() {
  11451. // clear the toolbar
  11452. this._clearManipulatorBar();
  11453. if (this.boundFunction) {
  11454. this.off('select', this.boundFunction);
  11455. }
  11456. // create the toolbar contents
  11457. this.manipulationDiv.innerHTML = "" +
  11458. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11459. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11460. "<div class='graph-seperatorLine'></div>" +
  11461. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11462. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  11463. // bind the icon
  11464. var backButton = document.getElementById("graph-manipulate-back");
  11465. backButton.onclick = this._createManipulatorBar.bind(this);
  11466. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11467. this.boundFunction = this._addNode.bind(this);
  11468. this.on('select', this.boundFunction);
  11469. },
  11470. /**
  11471. * create the toolbar to connect nodes
  11472. *
  11473. * @private
  11474. */
  11475. _createAddEdgeToolbar : function() {
  11476. // clear the toolbar
  11477. this._clearManipulatorBar();
  11478. this._unselectAll(true);
  11479. this.freezeSimulation = true;
  11480. if (this.boundFunction) {
  11481. this.off('select', this.boundFunction);
  11482. }
  11483. this._unselectAll();
  11484. this.forceAppendSelection = false;
  11485. this.blockConnectingEdgeSelection = true;
  11486. this.manipulationDiv.innerHTML = "" +
  11487. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11488. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11489. "<div class='graph-seperatorLine'></div>" +
  11490. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11491. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
  11492. // bind the icon
  11493. var backButton = document.getElementById("graph-manipulate-back");
  11494. backButton.onclick = this._createManipulatorBar.bind(this);
  11495. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11496. this.boundFunction = this._handleConnect.bind(this);
  11497. this.on('select', this.boundFunction);
  11498. // temporarily overload functions
  11499. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  11500. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  11501. this._handleTouch = this._handleConnect;
  11502. this._handleOnRelease = this._finishConnect;
  11503. // redraw to show the unselect
  11504. this._redraw();
  11505. },
  11506. /**
  11507. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  11508. * to walk the user through the process.
  11509. *
  11510. * @private
  11511. */
  11512. _handleConnect : function(pointer) {
  11513. if (this._getSelectedNodeCount() == 0) {
  11514. var node = this._getNodeAt(pointer);
  11515. if (node != null) {
  11516. if (node.clusterSize > 1) {
  11517. alert("Cannot create edges to a cluster.")
  11518. }
  11519. else {
  11520. this._selectObject(node,false);
  11521. // create a node the temporary line can look at
  11522. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  11523. this.sectors['support']['nodes']['targetNode'].x = node.x;
  11524. this.sectors['support']['nodes']['targetNode'].y = node.y;
  11525. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  11526. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  11527. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  11528. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  11529. // create a temporary edge
  11530. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  11531. this.edges['connectionEdge'].from = node;
  11532. this.edges['connectionEdge'].connected = true;
  11533. this.edges['connectionEdge'].smooth = true;
  11534. this.edges['connectionEdge'].selected = true;
  11535. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  11536. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  11537. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  11538. this._handleOnDrag = function(event) {
  11539. var pointer = this._getPointer(event.gesture.center);
  11540. this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
  11541. this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
  11542. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
  11543. this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
  11544. };
  11545. this.moving = true;
  11546. this.start();
  11547. }
  11548. }
  11549. }
  11550. },
  11551. _finishConnect : function(pointer) {
  11552. if (this._getSelectedNodeCount() == 1) {
  11553. // restore the drag function
  11554. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  11555. delete this.cachedFunctions["_handleOnDrag"];
  11556. // remember the edge id
  11557. var connectFromId = this.edges['connectionEdge'].fromId;
  11558. // remove the temporary nodes and edge
  11559. delete this.edges['connectionEdge'];
  11560. delete this.sectors['support']['nodes']['targetNode'];
  11561. delete this.sectors['support']['nodes']['targetViaNode'];
  11562. var node = this._getNodeAt(pointer);
  11563. if (node != null) {
  11564. if (node.clusterSize > 1) {
  11565. alert("Cannot create edges to a cluster.")
  11566. }
  11567. else {
  11568. this._createEdge(connectFromId,node.id);
  11569. this._createManipulatorBar();
  11570. }
  11571. }
  11572. this._unselectAll();
  11573. }
  11574. },
  11575. /**
  11576. * Adds a node on the specified location
  11577. *
  11578. * @param {Object} pointer
  11579. */
  11580. _addNode : function() {
  11581. if (this._selectionIsEmpty() && this.editMode == true) {
  11582. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  11583. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  11584. if (this.triggerFunctions.add) {
  11585. if (this.triggerFunctions.add.length == 2) {
  11586. var me = this;
  11587. this.triggerFunctions.add(defaultData, function(finalizedData) {
  11588. me.nodesData.add(finalizedData);
  11589. me._createManipulatorBar();
  11590. me.moving = true;
  11591. me.start();
  11592. });
  11593. }
  11594. else {
  11595. alert(this.constants.labels['addError']);
  11596. this._createManipulatorBar();
  11597. this.moving = true;
  11598. this.start();
  11599. }
  11600. }
  11601. else {
  11602. this.nodesData.add(defaultData);
  11603. this._createManipulatorBar();
  11604. this.moving = true;
  11605. this.start();
  11606. }
  11607. }
  11608. },
  11609. /**
  11610. * connect two nodes with a new edge.
  11611. *
  11612. * @private
  11613. */
  11614. _createEdge : function(sourceNodeId,targetNodeId) {
  11615. if (this.editMode == true) {
  11616. var defaultData = {from:sourceNodeId, to:targetNodeId};
  11617. if (this.triggerFunctions.connect) {
  11618. if (this.triggerFunctions.connect.length == 2) {
  11619. var me = this;
  11620. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  11621. me.edgesData.add(finalizedData);
  11622. me.moving = true;
  11623. me.start();
  11624. });
  11625. }
  11626. else {
  11627. alert(this.constants.labels["linkError"]);
  11628. this.moving = true;
  11629. this.start();
  11630. }
  11631. }
  11632. else {
  11633. this.edgesData.add(defaultData);
  11634. this.moving = true;
  11635. this.start();
  11636. }
  11637. }
  11638. },
  11639. /**
  11640. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  11641. *
  11642. * @private
  11643. */
  11644. _editNode : function() {
  11645. if (this.triggerFunctions.edit && this.editMode == true) {
  11646. var node = this._getSelectedNode();
  11647. var data = {id:node.id,
  11648. label: node.label,
  11649. group: node.group,
  11650. shape: node.shape,
  11651. color: {
  11652. background:node.color.background,
  11653. border:node.color.border,
  11654. highlight: {
  11655. background:node.color.highlight.background,
  11656. border:node.color.highlight.border
  11657. }
  11658. }};
  11659. if (this.triggerFunctions.edit.length == 2) {
  11660. var me = this;
  11661. this.triggerFunctions.edit(data, function (finalizedData) {
  11662. me.nodesData.update(finalizedData);
  11663. me._createManipulatorBar();
  11664. me.moving = true;
  11665. me.start();
  11666. });
  11667. }
  11668. else {
  11669. alert(this.constants.labels["editError"]);
  11670. }
  11671. }
  11672. else {
  11673. alert(this.constants.labels["editBoundError"]);
  11674. }
  11675. },
  11676. /**
  11677. * delete everything in the selection
  11678. *
  11679. * @private
  11680. */
  11681. _deleteSelected : function() {
  11682. if (!this._selectionIsEmpty() && this.editMode == true) {
  11683. if (!this._clusterInSelection()) {
  11684. var selectedNodes = this.getSelectedNodes();
  11685. var selectedEdges = this.getSelectedEdges();
  11686. if (this.triggerFunctions.del) {
  11687. var me = this;
  11688. var data = {nodes: selectedNodes, edges: selectedEdges};
  11689. if (this.triggerFunctions.del.length = 2) {
  11690. this.triggerFunctions.del(data, function (finalizedData) {
  11691. me.edgesData.remove(finalizedData.edges);
  11692. me.nodesData.remove(finalizedData.nodes);
  11693. me._unselectAll();
  11694. me.moving = true;
  11695. me.start();
  11696. });
  11697. }
  11698. else {
  11699. alert(this.constants.labels["deleteError"])
  11700. }
  11701. }
  11702. else {
  11703. this.edgesData.remove(selectedEdges);
  11704. this.nodesData.remove(selectedNodes);
  11705. this._unselectAll();
  11706. this.moving = true;
  11707. this.start();
  11708. }
  11709. }
  11710. else {
  11711. alert(this.constants.labels["deleteClusterError"]);
  11712. }
  11713. }
  11714. }
  11715. };
  11716. /**
  11717. * Creation of the SectorMixin var.
  11718. *
  11719. * This contains all the functions the Graph object can use to employ the sector system.
  11720. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  11721. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  11722. *
  11723. * Alex de Mulder
  11724. * 21-01-2013
  11725. */
  11726. var SectorMixin = {
  11727. /**
  11728. * This function is only called by the setData function of the Graph object.
  11729. * This loads the global references into the active sector. This initializes the sector.
  11730. *
  11731. * @private
  11732. */
  11733. _putDataInSector : function() {
  11734. this.sectors["active"][this._sector()].nodes = this.nodes;
  11735. this.sectors["active"][this._sector()].edges = this.edges;
  11736. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  11737. },
  11738. /**
  11739. * /**
  11740. * This function sets the global references to nodes, edges and nodeIndices back to
  11741. * those of the supplied (active) sector. If a type is defined, do the specific type
  11742. *
  11743. * @param {String} sectorId
  11744. * @param {String} [sectorType] | "active" or "frozen"
  11745. * @private
  11746. */
  11747. _switchToSector : function(sectorId, sectorType) {
  11748. if (sectorType === undefined || sectorType == "active") {
  11749. this._switchToActiveSector(sectorId);
  11750. }
  11751. else {
  11752. this._switchToFrozenSector(sectorId);
  11753. }
  11754. },
  11755. /**
  11756. * This function sets the global references to nodes, edges and nodeIndices back to
  11757. * those of the supplied active sector.
  11758. *
  11759. * @param sectorId
  11760. * @private
  11761. */
  11762. _switchToActiveSector : function(sectorId) {
  11763. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  11764. this.nodes = this.sectors["active"][sectorId]["nodes"];
  11765. this.edges = this.sectors["active"][sectorId]["edges"];
  11766. },
  11767. /**
  11768. * This function sets the global references to nodes, edges and nodeIndices back to
  11769. * those of the supplied active sector.
  11770. *
  11771. * @param sectorId
  11772. * @private
  11773. */
  11774. _switchToSupportSector : function() {
  11775. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  11776. this.nodes = this.sectors["support"]["nodes"];
  11777. this.edges = this.sectors["support"]["edges"];
  11778. },
  11779. /**
  11780. * This function sets the global references to nodes, edges and nodeIndices back to
  11781. * those of the supplied frozen sector.
  11782. *
  11783. * @param sectorId
  11784. * @private
  11785. */
  11786. _switchToFrozenSector : function(sectorId) {
  11787. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  11788. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  11789. this.edges = this.sectors["frozen"][sectorId]["edges"];
  11790. },
  11791. /**
  11792. * This function sets the global references to nodes, edges and nodeIndices back to
  11793. * those of the currently active sector.
  11794. *
  11795. * @private
  11796. */
  11797. _loadLatestSector : function() {
  11798. this._switchToSector(this._sector());
  11799. },
  11800. /**
  11801. * This function returns the currently active sector Id
  11802. *
  11803. * @returns {String}
  11804. * @private
  11805. */
  11806. _sector : function() {
  11807. return this.activeSector[this.activeSector.length-1];
  11808. },
  11809. /**
  11810. * This function returns the previously active sector Id
  11811. *
  11812. * @returns {String}
  11813. * @private
  11814. */
  11815. _previousSector : function() {
  11816. if (this.activeSector.length > 1) {
  11817. return this.activeSector[this.activeSector.length-2];
  11818. }
  11819. else {
  11820. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  11821. }
  11822. },
  11823. /**
  11824. * We add the active sector at the end of the this.activeSector array
  11825. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  11826. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  11827. *
  11828. * @param newId
  11829. * @private
  11830. */
  11831. _setActiveSector : function(newId) {
  11832. this.activeSector.push(newId);
  11833. },
  11834. /**
  11835. * We remove the currently active sector id from the active sector stack. This happens when
  11836. * we reactivate the previously active sector
  11837. *
  11838. * @private
  11839. */
  11840. _forgetLastSector : function() {
  11841. this.activeSector.pop();
  11842. },
  11843. /**
  11844. * This function creates a new active sector with the supplied newId. This newId
  11845. * is the expanding node id.
  11846. *
  11847. * @param {String} newId | Id of the new active sector
  11848. * @private
  11849. */
  11850. _createNewSector : function(newId) {
  11851. // create the new sector
  11852. this.sectors["active"][newId] = {"nodes":{},
  11853. "edges":{},
  11854. "nodeIndices":[],
  11855. "formationScale": this.scale,
  11856. "drawingNode": undefined};
  11857. // create the new sector render node. This gives visual feedback that you are in a new sector.
  11858. this.sectors["active"][newId]['drawingNode'] = new Node(
  11859. {id:newId,
  11860. color: {
  11861. background: "#eaefef",
  11862. border: "495c5e"
  11863. }
  11864. },{},{},this.constants);
  11865. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  11866. },
  11867. /**
  11868. * This function removes the currently active sector. This is called when we create a new
  11869. * active sector.
  11870. *
  11871. * @param {String} sectorId | Id of the active sector that will be removed
  11872. * @private
  11873. */
  11874. _deleteActiveSector : function(sectorId) {
  11875. delete this.sectors["active"][sectorId];
  11876. },
  11877. /**
  11878. * This function removes the currently active sector. This is called when we reactivate
  11879. * the previously active sector.
  11880. *
  11881. * @param {String} sectorId | Id of the active sector that will be removed
  11882. * @private
  11883. */
  11884. _deleteFrozenSector : function(sectorId) {
  11885. delete this.sectors["frozen"][sectorId];
  11886. },
  11887. /**
  11888. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  11889. * We copy the references, then delete the active entree.
  11890. *
  11891. * @param sectorId
  11892. * @private
  11893. */
  11894. _freezeSector : function(sectorId) {
  11895. // we move the set references from the active to the frozen stack.
  11896. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  11897. // we have moved the sector data into the frozen set, we now remove it from the active set
  11898. this._deleteActiveSector(sectorId);
  11899. },
  11900. /**
  11901. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  11902. * object to the "active" object.
  11903. *
  11904. * @param sectorId
  11905. * @private
  11906. */
  11907. _activateSector : function(sectorId) {
  11908. // we move the set references from the frozen to the active stack.
  11909. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  11910. // we have moved the sector data into the active set, we now remove it from the frozen stack
  11911. this._deleteFrozenSector(sectorId);
  11912. },
  11913. /**
  11914. * This function merges the data from the currently active sector with a frozen sector. This is used
  11915. * in the process of reverting back to the previously active sector.
  11916. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  11917. * upon the creation of a new active sector.
  11918. *
  11919. * @param sectorId
  11920. * @private
  11921. */
  11922. _mergeThisWithFrozen : function(sectorId) {
  11923. // copy all nodes
  11924. for (var nodeId in this.nodes) {
  11925. if (this.nodes.hasOwnProperty(nodeId)) {
  11926. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  11927. }
  11928. }
  11929. // copy all edges (if not fully clustered, else there are no edges)
  11930. for (var edgeId in this.edges) {
  11931. if (this.edges.hasOwnProperty(edgeId)) {
  11932. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  11933. }
  11934. }
  11935. // merge the nodeIndices
  11936. for (var i = 0; i < this.nodeIndices.length; i++) {
  11937. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  11938. }
  11939. },
  11940. /**
  11941. * This clusters the sector to one cluster. It was a single cluster before this process started so
  11942. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  11943. *
  11944. * @private
  11945. */
  11946. _collapseThisToSingleCluster : function() {
  11947. this.clusterToFit(1,false);
  11948. },
  11949. /**
  11950. * We create a new active sector from the node that we want to open.
  11951. *
  11952. * @param node
  11953. * @private
  11954. */
  11955. _addSector : function(node) {
  11956. // this is the currently active sector
  11957. var sector = this._sector();
  11958. // // this should allow me to select nodes from a frozen set.
  11959. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  11960. // console.log("the node is part of the active sector");
  11961. // }
  11962. // else {
  11963. // console.log("I dont know what happened!!");
  11964. // }
  11965. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  11966. delete this.nodes[node.id];
  11967. var unqiueIdentifier = util.randomUUID();
  11968. // we fully freeze the currently active sector
  11969. this._freezeSector(sector);
  11970. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  11971. this._createNewSector(unqiueIdentifier);
  11972. // we add the active sector to the sectors array to be able to revert these steps later on
  11973. this._setActiveSector(unqiueIdentifier);
  11974. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  11975. this._switchToSector(this._sector());
  11976. // finally we add the node we removed from our previous active sector to the new active sector
  11977. this.nodes[node.id] = node;
  11978. },
  11979. /**
  11980. * We close the sector that is currently open and revert back to the one before.
  11981. * If the active sector is the "default" sector, nothing happens.
  11982. *
  11983. * @private
  11984. */
  11985. _collapseSector : function() {
  11986. // the currently active sector
  11987. var sector = this._sector();
  11988. // we cannot collapse the default sector
  11989. if (sector != "default") {
  11990. if ((this.nodeIndices.length == 1) ||
  11991. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11992. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11993. var previousSector = this._previousSector();
  11994. // we collapse the sector back to a single cluster
  11995. this._collapseThisToSingleCluster();
  11996. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  11997. // This previous sector is the one we will reactivate
  11998. this._mergeThisWithFrozen(previousSector);
  11999. // the previously active (frozen) sector now has all the data from the currently active sector.
  12000. // we can now delete the active sector.
  12001. this._deleteActiveSector(sector);
  12002. // we activate the previously active (and currently frozen) sector.
  12003. this._activateSector(previousSector);
  12004. // we load the references from the newly active sector into the global references
  12005. this._switchToSector(previousSector);
  12006. // we forget the previously active sector because we reverted to the one before
  12007. this._forgetLastSector();
  12008. // finally, we update the node index list.
  12009. this._updateNodeIndexList();
  12010. // we refresh the list with calulation nodes and calculation node indices.
  12011. this._updateCalculationNodes();
  12012. }
  12013. }
  12014. },
  12015. /**
  12016. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  12017. *
  12018. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12019. * | we dont pass the function itself because then the "this" is the window object
  12020. * | instead of the Graph object
  12021. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12022. * @private
  12023. */
  12024. _doInAllActiveSectors : function(runFunction,argument) {
  12025. if (argument === undefined) {
  12026. for (var sector in this.sectors["active"]) {
  12027. if (this.sectors["active"].hasOwnProperty(sector)) {
  12028. // switch the global references to those of this sector
  12029. this._switchToActiveSector(sector);
  12030. this[runFunction]();
  12031. }
  12032. }
  12033. }
  12034. else {
  12035. for (var sector in this.sectors["active"]) {
  12036. if (this.sectors["active"].hasOwnProperty(sector)) {
  12037. // switch the global references to those of this sector
  12038. this._switchToActiveSector(sector);
  12039. var args = Array.prototype.splice.call(arguments, 1);
  12040. if (args.length > 1) {
  12041. this[runFunction](args[0],args[1]);
  12042. }
  12043. else {
  12044. this[runFunction](argument);
  12045. }
  12046. }
  12047. }
  12048. }
  12049. // we revert the global references back to our active sector
  12050. this._loadLatestSector();
  12051. },
  12052. /**
  12053. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  12054. *
  12055. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12056. * | we dont pass the function itself because then the "this" is the window object
  12057. * | instead of the Graph object
  12058. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12059. * @private
  12060. */
  12061. _doInSupportSector : function(runFunction,argument) {
  12062. if (argument === undefined) {
  12063. this._switchToSupportSector();
  12064. this[runFunction]();
  12065. }
  12066. else {
  12067. this._switchToSupportSector();
  12068. var args = Array.prototype.splice.call(arguments, 1);
  12069. if (args.length > 1) {
  12070. this[runFunction](args[0],args[1]);
  12071. }
  12072. else {
  12073. this[runFunction](argument);
  12074. }
  12075. }
  12076. // we revert the global references back to our active sector
  12077. this._loadLatestSector();
  12078. },
  12079. /**
  12080. * This runs a function in all frozen sectors. This is used in the _redraw().
  12081. *
  12082. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12083. * | we don't pass the function itself because then the "this" is the window object
  12084. * | instead of the Graph object
  12085. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12086. * @private
  12087. */
  12088. _doInAllFrozenSectors : function(runFunction,argument) {
  12089. if (argument === undefined) {
  12090. for (var sector in this.sectors["frozen"]) {
  12091. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  12092. // switch the global references to those of this sector
  12093. this._switchToFrozenSector(sector);
  12094. this[runFunction]();
  12095. }
  12096. }
  12097. }
  12098. else {
  12099. for (var sector in this.sectors["frozen"]) {
  12100. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  12101. // switch the global references to those of this sector
  12102. this._switchToFrozenSector(sector);
  12103. var args = Array.prototype.splice.call(arguments, 1);
  12104. if (args.length > 1) {
  12105. this[runFunction](args[0],args[1]);
  12106. }
  12107. else {
  12108. this[runFunction](argument);
  12109. }
  12110. }
  12111. }
  12112. }
  12113. this._loadLatestSector();
  12114. },
  12115. /**
  12116. * This runs a function in all sectors. This is used in the _redraw().
  12117. *
  12118. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  12119. * | we don't pass the function itself because then the "this" is the window object
  12120. * | instead of the Graph object
  12121. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  12122. * @private
  12123. */
  12124. _doInAllSectors : function(runFunction,argument) {
  12125. var args = Array.prototype.splice.call(arguments, 1);
  12126. if (argument === undefined) {
  12127. this._doInAllActiveSectors(runFunction);
  12128. this._doInAllFrozenSectors(runFunction);
  12129. }
  12130. else {
  12131. if (args.length > 1) {
  12132. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  12133. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  12134. }
  12135. else {
  12136. this._doInAllActiveSectors(runFunction,argument);
  12137. this._doInAllFrozenSectors(runFunction,argument);
  12138. }
  12139. }
  12140. },
  12141. /**
  12142. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  12143. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  12144. *
  12145. * @private
  12146. */
  12147. _clearNodeIndexList : function() {
  12148. var sector = this._sector();
  12149. this.sectors["active"][sector]["nodeIndices"] = [];
  12150. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  12151. },
  12152. /**
  12153. * Draw the encompassing sector node
  12154. *
  12155. * @param ctx
  12156. * @param sectorType
  12157. * @private
  12158. */
  12159. _drawSectorNodes : function(ctx,sectorType) {
  12160. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  12161. for (var sector in this.sectors[sectorType]) {
  12162. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  12163. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  12164. this._switchToSector(sector,sectorType);
  12165. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  12166. for (var nodeId in this.nodes) {
  12167. if (this.nodes.hasOwnProperty(nodeId)) {
  12168. node = this.nodes[nodeId];
  12169. node.resize(ctx);
  12170. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  12171. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  12172. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  12173. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  12174. }
  12175. }
  12176. node = this.sectors[sectorType][sector]["drawingNode"];
  12177. node.x = 0.5 * (maxX + minX);
  12178. node.y = 0.5 * (maxY + minY);
  12179. node.width = 2 * (node.x - minX);
  12180. node.height = 2 * (node.y - minY);
  12181. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  12182. node.setScale(this.scale);
  12183. node._drawCircle(ctx);
  12184. }
  12185. }
  12186. }
  12187. },
  12188. _drawAllSectorNodes : function(ctx) {
  12189. this._drawSectorNodes(ctx,"frozen");
  12190. this._drawSectorNodes(ctx,"active");
  12191. this._loadLatestSector();
  12192. }
  12193. };
  12194. /**
  12195. * Creation of the ClusterMixin var.
  12196. *
  12197. * This contains all the functions the Graph object can use to employ clustering
  12198. *
  12199. * Alex de Mulder
  12200. * 21-01-2013
  12201. */
  12202. var ClusterMixin = {
  12203. /**
  12204. * This is only called in the constructor of the graph object
  12205. *
  12206. */
  12207. startWithClustering : function() {
  12208. // cluster if the data set is big
  12209. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  12210. // updates the lables after clustering
  12211. this.updateLabels();
  12212. // this is called here because if clusterin is disabled, the start and stabilize are called in
  12213. // the setData function.
  12214. if (this.stabilize) {
  12215. this._stabilize();
  12216. }
  12217. this.start();
  12218. },
  12219. /**
  12220. * This function clusters until the initialMaxNodes has been reached
  12221. *
  12222. * @param {Number} maxNumberOfNodes
  12223. * @param {Boolean} reposition
  12224. */
  12225. clusterToFit : function(maxNumberOfNodes, reposition) {
  12226. var numberOfNodes = this.nodeIndices.length;
  12227. var maxLevels = 50;
  12228. var level = 0;
  12229. // we first cluster the hubs, then we pull in the outliers, repeat
  12230. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  12231. if (level % 3 == 0) {
  12232. this.forceAggregateHubs(true);
  12233. this.normalizeClusterLevels();
  12234. }
  12235. else {
  12236. this.increaseClusterLevel(); // this also includes a cluster normalization
  12237. }
  12238. numberOfNodes = this.nodeIndices.length;
  12239. level += 1;
  12240. }
  12241. // after the clustering we reposition the nodes to reduce the initial chaos
  12242. if (level > 0 && reposition == true) {
  12243. this.repositionNodes();
  12244. }
  12245. this._updateCalculationNodes();
  12246. },
  12247. /**
  12248. * This function can be called to open up a specific cluster. It is only called by
  12249. * It will unpack the cluster back one level.
  12250. *
  12251. * @param node | Node object: cluster to open.
  12252. */
  12253. openCluster : function(node) {
  12254. var isMovingBeforeClustering = this.moving;
  12255. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  12256. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  12257. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  12258. this._addSector(node);
  12259. var level = 0;
  12260. // we decluster until we reach a decent number of nodes
  12261. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  12262. this.decreaseClusterLevel();
  12263. level += 1;
  12264. }
  12265. }
  12266. else {
  12267. this._expandClusterNode(node,false,true);
  12268. // update the index list, dynamic edges and labels
  12269. this._updateNodeIndexList();
  12270. this._updateDynamicEdges();
  12271. this._updateCalculationNodes();
  12272. this.updateLabels();
  12273. }
  12274. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12275. if (this.moving != isMovingBeforeClustering) {
  12276. this.start();
  12277. }
  12278. },
  12279. /**
  12280. * This calls the updateClustes with default arguments
  12281. */
  12282. updateClustersDefault : function() {
  12283. if (this.constants.clustering.enabled == true) {
  12284. this.updateClusters(0,false,false);
  12285. }
  12286. },
  12287. /**
  12288. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  12289. * be clustered with their connected node. This can be repeated as many times as needed.
  12290. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  12291. */
  12292. increaseClusterLevel : function() {
  12293. this.updateClusters(-1,false,true);
  12294. },
  12295. /**
  12296. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  12297. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  12298. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  12299. */
  12300. decreaseClusterLevel : function() {
  12301. this.updateClusters(1,false,true);
  12302. },
  12303. /**
  12304. * This is the main clustering function. It clusters and declusters on zoom or forced
  12305. * This function clusters on zoom, it can be called with a predefined zoom direction
  12306. * If out, check if we can form clusters, if in, check if we can open clusters.
  12307. * This function is only called from _zoom()
  12308. *
  12309. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  12310. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  12311. * @param {Boolean} force | enabled or disable forcing
  12312. *
  12313. */
  12314. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  12315. var isMovingBeforeClustering = this.moving;
  12316. var amountOfNodes = this.nodeIndices.length;
  12317. // on zoom out collapse the sector if the scale is at the level the sector was made
  12318. if (this.previousScale > this.scale && zoomDirection == 0) {
  12319. this._collapseSector();
  12320. }
  12321. // check if we zoom in or out
  12322. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12323. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  12324. // outer nodes determines if it is being clustered
  12325. this._formClusters(force);
  12326. }
  12327. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  12328. if (force == true) {
  12329. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  12330. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  12331. this._openClusters(recursive,force);
  12332. }
  12333. else {
  12334. // if a cluster takes up a set percentage of the active window
  12335. this._openClustersBySize();
  12336. }
  12337. }
  12338. this._updateNodeIndexList();
  12339. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  12340. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  12341. this._aggregateHubs(force);
  12342. this._updateNodeIndexList();
  12343. }
  12344. // we now reduce chains.
  12345. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12346. this.handleChains();
  12347. this._updateNodeIndexList();
  12348. }
  12349. this.previousScale = this.scale;
  12350. // rest of the update the index list, dynamic edges and labels
  12351. this._updateDynamicEdges();
  12352. this.updateLabels();
  12353. // if a cluster was formed, we increase the clusterSession
  12354. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  12355. this.clusterSession += 1;
  12356. // if clusters have been made, we normalize the cluster level
  12357. this.normalizeClusterLevels();
  12358. }
  12359. if (doNotStart == false || doNotStart === undefined) {
  12360. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12361. if (this.moving != isMovingBeforeClustering) {
  12362. this.start();
  12363. }
  12364. }
  12365. this._updateCalculationNodes();
  12366. },
  12367. /**
  12368. * This function handles the chains. It is called on every updateClusters().
  12369. */
  12370. handleChains : function() {
  12371. // after clustering we check how many chains there are
  12372. var chainPercentage = this._getChainFraction();
  12373. if (chainPercentage > this.constants.clustering.chainThreshold) {
  12374. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  12375. }
  12376. },
  12377. /**
  12378. * this functions starts clustering by hubs
  12379. * The minimum hub threshold is set globally
  12380. *
  12381. * @private
  12382. */
  12383. _aggregateHubs : function(force) {
  12384. this._getHubSize();
  12385. this._formClustersByHub(force,false);
  12386. },
  12387. /**
  12388. * This function is fired by keypress. It forces hubs to form.
  12389. *
  12390. */
  12391. forceAggregateHubs : function(doNotStart) {
  12392. var isMovingBeforeClustering = this.moving;
  12393. var amountOfNodes = this.nodeIndices.length;
  12394. this._aggregateHubs(true);
  12395. // update the index list, dynamic edges and labels
  12396. this._updateNodeIndexList();
  12397. this._updateDynamicEdges();
  12398. this.updateLabels();
  12399. // if a cluster was formed, we increase the clusterSession
  12400. if (this.nodeIndices.length != amountOfNodes) {
  12401. this.clusterSession += 1;
  12402. }
  12403. if (doNotStart == false || doNotStart === undefined) {
  12404. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12405. if (this.moving != isMovingBeforeClustering) {
  12406. this.start();
  12407. }
  12408. }
  12409. },
  12410. /**
  12411. * If a cluster takes up more than a set percentage of the screen, open the cluster
  12412. *
  12413. * @private
  12414. */
  12415. _openClustersBySize : function() {
  12416. for (var nodeId in this.nodes) {
  12417. if (this.nodes.hasOwnProperty(nodeId)) {
  12418. var node = this.nodes[nodeId];
  12419. if (node.inView() == true) {
  12420. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  12421. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  12422. this.openCluster(node);
  12423. }
  12424. }
  12425. }
  12426. }
  12427. },
  12428. /**
  12429. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  12430. * has to be opened based on the current zoom level.
  12431. *
  12432. * @private
  12433. */
  12434. _openClusters : function(recursive,force) {
  12435. for (var i = 0; i < this.nodeIndices.length; i++) {
  12436. var node = this.nodes[this.nodeIndices[i]];
  12437. this._expandClusterNode(node,recursive,force);
  12438. this._updateCalculationNodes();
  12439. }
  12440. },
  12441. /**
  12442. * This function checks if a node has to be opened. This is done by checking the zoom level.
  12443. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  12444. * This recursive behaviour is optional and can be set by the recursive argument.
  12445. *
  12446. * @param {Node} parentNode | to check for cluster and expand
  12447. * @param {Boolean} recursive | enabled or disable recursive calling
  12448. * @param {Boolean} force | enabled or disable forcing
  12449. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  12450. * @private
  12451. */
  12452. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  12453. // first check if node is a cluster
  12454. if (parentNode.clusterSize > 1) {
  12455. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  12456. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  12457. openAll = true;
  12458. }
  12459. recursive = openAll ? true : recursive;
  12460. // if the last child has been added on a smaller scale than current scale decluster
  12461. if (parentNode.formationScale < this.scale || force == true) {
  12462. // we will check if any of the contained child nodes should be removed from the cluster
  12463. for (var containedNodeId in parentNode.containedNodes) {
  12464. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  12465. var childNode = parentNode.containedNodes[containedNodeId];
  12466. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  12467. // the largest cluster is the one that comes from outside
  12468. if (force == true) {
  12469. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  12470. || openAll) {
  12471. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12472. }
  12473. }
  12474. else {
  12475. if (this._nodeInActiveArea(parentNode)) {
  12476. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12477. }
  12478. }
  12479. }
  12480. }
  12481. }
  12482. }
  12483. },
  12484. /**
  12485. * ONLY CALLED FROM _expandClusterNode
  12486. *
  12487. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  12488. * the child node from the parent contained_node object and put it back into the global nodes object.
  12489. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  12490. *
  12491. * @param {Node} parentNode | the parent node
  12492. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  12493. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  12494. * With force and recursive both true, the entire cluster is unpacked
  12495. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  12496. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  12497. * @private
  12498. */
  12499. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  12500. var childNode = parentNode.containedNodes[containedNodeId];
  12501. // if child node has been added on smaller scale than current, kick out
  12502. if (childNode.formationScale < this.scale || force == true) {
  12503. // unselect all selected items
  12504. this._unselectAll();
  12505. // put the child node back in the global nodes object
  12506. this.nodes[containedNodeId] = childNode;
  12507. // release the contained edges from this childNode back into the global edges
  12508. this._releaseContainedEdges(parentNode,childNode);
  12509. // reconnect rerouted edges to the childNode
  12510. this._connectEdgeBackToChild(parentNode,childNode);
  12511. // validate all edges in dynamicEdges
  12512. this._validateEdges(parentNode);
  12513. // undo the changes from the clustering operation on the parent node
  12514. parentNode.mass -= childNode.mass;
  12515. parentNode.clusterSize -= childNode.clusterSize;
  12516. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12517. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  12518. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  12519. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  12520. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  12521. // remove node from the list
  12522. delete parentNode.containedNodes[containedNodeId];
  12523. // check if there are other childs with this clusterSession in the parent.
  12524. var othersPresent = false;
  12525. for (var childNodeId in parentNode.containedNodes) {
  12526. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  12527. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  12528. othersPresent = true;
  12529. break;
  12530. }
  12531. }
  12532. }
  12533. // if there are no others, remove the cluster session from the list
  12534. if (othersPresent == false) {
  12535. parentNode.clusterSessions.pop();
  12536. }
  12537. this._repositionBezierNodes(childNode);
  12538. // this._repositionBezierNodes(parentNode);
  12539. // remove the clusterSession from the child node
  12540. childNode.clusterSession = 0;
  12541. // recalculate the size of the node on the next time the node is rendered
  12542. parentNode.clearSizeCache();
  12543. // restart the simulation to reorganise all nodes
  12544. this.moving = true;
  12545. }
  12546. // check if a further expansion step is possible if recursivity is enabled
  12547. if (recursive == true) {
  12548. this._expandClusterNode(childNode,recursive,force,openAll);
  12549. }
  12550. },
  12551. /**
  12552. * position the bezier nodes at the center of the edges
  12553. *
  12554. * @param node
  12555. * @private
  12556. */
  12557. _repositionBezierNodes : function(node) {
  12558. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12559. node.dynamicEdges[i].positionBezierNode();
  12560. }
  12561. },
  12562. /**
  12563. * This function checks if any nodes at the end of their trees have edges below a threshold length
  12564. * This function is called only from updateClusters()
  12565. * forceLevelCollapse ignores the length of the edge and collapses one level
  12566. * This means that a node with only one edge will be clustered with its connected node
  12567. *
  12568. * @private
  12569. * @param {Boolean} force
  12570. */
  12571. _formClusters : function(force) {
  12572. if (force == false) {
  12573. this._formClustersByZoom();
  12574. }
  12575. else {
  12576. this._forceClustersByZoom();
  12577. }
  12578. },
  12579. /**
  12580. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  12581. *
  12582. * @private
  12583. */
  12584. _formClustersByZoom : function() {
  12585. var dx,dy,length,
  12586. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12587. // check if any edges are shorter than minLength and start the clustering
  12588. // the clustering favours the node with the larger mass
  12589. for (var edgeId in this.edges) {
  12590. if (this.edges.hasOwnProperty(edgeId)) {
  12591. var edge = this.edges[edgeId];
  12592. if (edge.connected) {
  12593. if (edge.toId != edge.fromId) {
  12594. dx = (edge.to.x - edge.from.x);
  12595. dy = (edge.to.y - edge.from.y);
  12596. length = Math.sqrt(dx * dx + dy * dy);
  12597. if (length < minLength) {
  12598. // first check which node is larger
  12599. var parentNode = edge.from;
  12600. var childNode = edge.to;
  12601. if (edge.to.mass > edge.from.mass) {
  12602. parentNode = edge.to;
  12603. childNode = edge.from;
  12604. }
  12605. if (childNode.dynamicEdgesLength == 1) {
  12606. this._addToCluster(parentNode,childNode,false);
  12607. }
  12608. else if (parentNode.dynamicEdgesLength == 1) {
  12609. this._addToCluster(childNode,parentNode,false);
  12610. }
  12611. }
  12612. }
  12613. }
  12614. }
  12615. }
  12616. },
  12617. /**
  12618. * This function forces the graph to cluster all nodes with only one connecting edge to their
  12619. * connected node.
  12620. *
  12621. * @private
  12622. */
  12623. _forceClustersByZoom : function() {
  12624. for (var nodeId in this.nodes) {
  12625. // another node could have absorbed this child.
  12626. if (this.nodes.hasOwnProperty(nodeId)) {
  12627. var childNode = this.nodes[nodeId];
  12628. // the edges can be swallowed by another decrease
  12629. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  12630. var edge = childNode.dynamicEdges[0];
  12631. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  12632. // group to the largest node
  12633. if (childNode.id != parentNode.id) {
  12634. if (parentNode.mass > childNode.mass) {
  12635. this._addToCluster(parentNode,childNode,true);
  12636. }
  12637. else {
  12638. this._addToCluster(childNode,parentNode,true);
  12639. }
  12640. }
  12641. }
  12642. }
  12643. }
  12644. },
  12645. /**
  12646. * To keep the nodes of roughly equal size we normalize the cluster levels.
  12647. * This function clusters a node to its smallest connected neighbour.
  12648. *
  12649. * @param node
  12650. * @private
  12651. */
  12652. _clusterToSmallestNeighbour : function(node) {
  12653. var smallestNeighbour = -1;
  12654. var smallestNeighbourNode = null;
  12655. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12656. if (node.dynamicEdges[i] !== undefined) {
  12657. var neighbour = null;
  12658. if (node.dynamicEdges[i].fromId != node.id) {
  12659. neighbour = node.dynamicEdges[i].from;
  12660. }
  12661. else if (node.dynamicEdges[i].toId != node.id) {
  12662. neighbour = node.dynamicEdges[i].to;
  12663. }
  12664. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  12665. smallestNeighbour = neighbour.clusterSessions.length;
  12666. smallestNeighbourNode = neighbour;
  12667. }
  12668. }
  12669. }
  12670. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  12671. this._addToCluster(neighbour, node, true);
  12672. }
  12673. },
  12674. /**
  12675. * This function forms clusters from hubs, it loops over all nodes
  12676. *
  12677. * @param {Boolean} force | Disregard zoom level
  12678. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12679. * @private
  12680. */
  12681. _formClustersByHub : function(force, onlyEqual) {
  12682. // we loop over all nodes in the list
  12683. for (var nodeId in this.nodes) {
  12684. // we check if it is still available since it can be used by the clustering in this loop
  12685. if (this.nodes.hasOwnProperty(nodeId)) {
  12686. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  12687. }
  12688. }
  12689. },
  12690. /**
  12691. * This function forms a cluster from a specific preselected hub node
  12692. *
  12693. * @param {Node} hubNode | the node we will cluster as a hub
  12694. * @param {Boolean} force | Disregard zoom level
  12695. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12696. * @param {Number} [absorptionSizeOffset] |
  12697. * @private
  12698. */
  12699. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  12700. if (absorptionSizeOffset === undefined) {
  12701. absorptionSizeOffset = 0;
  12702. }
  12703. // we decide if the node is a hub
  12704. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  12705. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  12706. // initialize variables
  12707. var dx,dy,length;
  12708. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12709. var allowCluster = false;
  12710. // we create a list of edges because the dynamicEdges change over the course of this loop
  12711. var edgesIdarray = [];
  12712. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  12713. for (var j = 0; j < amountOfInitialEdges; j++) {
  12714. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  12715. }
  12716. // if the hub clustering is not forces, we check if one of the edges connected
  12717. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  12718. if (force == false) {
  12719. allowCluster = false;
  12720. for (j = 0; j < amountOfInitialEdges; j++) {
  12721. var edge = this.edges[edgesIdarray[j]];
  12722. if (edge !== undefined) {
  12723. if (edge.connected) {
  12724. if (edge.toId != edge.fromId) {
  12725. dx = (edge.to.x - edge.from.x);
  12726. dy = (edge.to.y - edge.from.y);
  12727. length = Math.sqrt(dx * dx + dy * dy);
  12728. if (length < minLength) {
  12729. allowCluster = true;
  12730. break;
  12731. }
  12732. }
  12733. }
  12734. }
  12735. }
  12736. }
  12737. // start the clustering if allowed
  12738. if ((!force && allowCluster) || force) {
  12739. // we loop over all edges INITIALLY connected to this hub
  12740. for (j = 0; j < amountOfInitialEdges; j++) {
  12741. edge = this.edges[edgesIdarray[j]];
  12742. // the edge can be clustered by this function in a previous loop
  12743. if (edge !== undefined) {
  12744. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  12745. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  12746. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  12747. (childNode.id != hubNode.id)) {
  12748. this._addToCluster(hubNode,childNode,force);
  12749. }
  12750. }
  12751. }
  12752. }
  12753. }
  12754. },
  12755. /**
  12756. * This function adds the child node to the parent node, creating a cluster if it is not already.
  12757. *
  12758. * @param {Node} parentNode | this is the node that will house the child node
  12759. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  12760. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  12761. * @private
  12762. */
  12763. _addToCluster : function(parentNode, childNode, force) {
  12764. // join child node in the parent node
  12765. parentNode.containedNodes[childNode.id] = childNode;
  12766. // manage all the edges connected to the child and parent nodes
  12767. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  12768. var edge = childNode.dynamicEdges[i];
  12769. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  12770. this._addToContainedEdges(parentNode,childNode,edge);
  12771. }
  12772. else {
  12773. this._connectEdgeToCluster(parentNode,childNode,edge);
  12774. }
  12775. }
  12776. // a contained node has no dynamic edges.
  12777. childNode.dynamicEdges = [];
  12778. // remove circular edges from clusters
  12779. this._containCircularEdgesFromNode(parentNode,childNode);
  12780. // remove the childNode from the global nodes object
  12781. delete this.nodes[childNode.id];
  12782. // update the properties of the child and parent
  12783. var massBefore = parentNode.mass;
  12784. childNode.clusterSession = this.clusterSession;
  12785. parentNode.mass += childNode.mass;
  12786. parentNode.clusterSize += childNode.clusterSize;
  12787. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12788. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  12789. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  12790. parentNode.clusterSessions.push(this.clusterSession);
  12791. }
  12792. // forced clusters only open from screen size and double tap
  12793. if (force == true) {
  12794. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  12795. parentNode.formationScale = 0;
  12796. }
  12797. else {
  12798. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  12799. }
  12800. // recalculate the size of the node on the next time the node is rendered
  12801. parentNode.clearSizeCache();
  12802. // set the pop-out scale for the childnode
  12803. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  12804. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  12805. childNode.clearVelocity();
  12806. // the mass has altered, preservation of energy dictates the velocity to be updated
  12807. parentNode.updateVelocity(massBefore);
  12808. // restart the simulation to reorganise all nodes
  12809. this.moving = true;
  12810. },
  12811. /**
  12812. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  12813. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  12814. * It has to be called if a level is collapsed. It is called by _formClusters().
  12815. * @private
  12816. */
  12817. _updateDynamicEdges : function() {
  12818. for (var i = 0; i < this.nodeIndices.length; i++) {
  12819. var node = this.nodes[this.nodeIndices[i]];
  12820. node.dynamicEdgesLength = node.dynamicEdges.length;
  12821. // this corrects for multiple edges pointing at the same other node
  12822. var correction = 0;
  12823. if (node.dynamicEdgesLength > 1) {
  12824. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  12825. var edgeToId = node.dynamicEdges[j].toId;
  12826. var edgeFromId = node.dynamicEdges[j].fromId;
  12827. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  12828. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  12829. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  12830. correction += 1;
  12831. }
  12832. }
  12833. }
  12834. }
  12835. node.dynamicEdgesLength -= correction;
  12836. }
  12837. },
  12838. /**
  12839. * This adds an edge from the childNode to the contained edges of the parent node
  12840. *
  12841. * @param parentNode | Node object
  12842. * @param childNode | Node object
  12843. * @param edge | Edge object
  12844. * @private
  12845. */
  12846. _addToContainedEdges : function(parentNode, childNode, edge) {
  12847. // create an array object if it does not yet exist for this childNode
  12848. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  12849. parentNode.containedEdges[childNode.id] = []
  12850. }
  12851. // add this edge to the list
  12852. parentNode.containedEdges[childNode.id].push(edge);
  12853. // remove the edge from the global edges object
  12854. delete this.edges[edge.id];
  12855. // remove the edge from the parent object
  12856. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12857. if (parentNode.dynamicEdges[i].id == edge.id) {
  12858. parentNode.dynamicEdges.splice(i,1);
  12859. break;
  12860. }
  12861. }
  12862. },
  12863. /**
  12864. * This function connects an edge that was connected to a child node to the parent node.
  12865. * It keeps track of which nodes it has been connected to with the originalId array.
  12866. *
  12867. * @param {Node} parentNode | Node object
  12868. * @param {Node} childNode | Node object
  12869. * @param {Edge} edge | Edge object
  12870. * @private
  12871. */
  12872. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  12873. // handle circular edges
  12874. if (edge.toId == edge.fromId) {
  12875. this._addToContainedEdges(parentNode, childNode, edge);
  12876. }
  12877. else {
  12878. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  12879. edge.originalToId.push(childNode.id);
  12880. edge.to = parentNode;
  12881. edge.toId = parentNode.id;
  12882. }
  12883. else { // edge connected to other node with the "from" side
  12884. edge.originalFromId.push(childNode.id);
  12885. edge.from = parentNode;
  12886. edge.fromId = parentNode.id;
  12887. }
  12888. this._addToReroutedEdges(parentNode,childNode,edge);
  12889. }
  12890. },
  12891. /**
  12892. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  12893. * these edges inside of the cluster.
  12894. *
  12895. * @param parentNode
  12896. * @param childNode
  12897. * @private
  12898. */
  12899. _containCircularEdgesFromNode : function(parentNode, childNode) {
  12900. // manage all the edges connected to the child and parent nodes
  12901. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12902. var edge = parentNode.dynamicEdges[i];
  12903. // handle circular edges
  12904. if (edge.toId == edge.fromId) {
  12905. this._addToContainedEdges(parentNode, childNode, edge);
  12906. }
  12907. }
  12908. },
  12909. /**
  12910. * This adds an edge from the childNode to the rerouted edges of the parent node
  12911. *
  12912. * @param parentNode | Node object
  12913. * @param childNode | Node object
  12914. * @param edge | Edge object
  12915. * @private
  12916. */
  12917. _addToReroutedEdges : function(parentNode, childNode, edge) {
  12918. // create an array object if it does not yet exist for this childNode
  12919. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  12920. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  12921. parentNode.reroutedEdges[childNode.id] = [];
  12922. }
  12923. parentNode.reroutedEdges[childNode.id].push(edge);
  12924. // this edge becomes part of the dynamicEdges of the cluster node
  12925. parentNode.dynamicEdges.push(edge);
  12926. },
  12927. /**
  12928. * This function connects an edge that was connected to a cluster node back to the child node.
  12929. *
  12930. * @param parentNode | Node object
  12931. * @param childNode | Node object
  12932. * @private
  12933. */
  12934. _connectEdgeBackToChild : function(parentNode, childNode) {
  12935. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  12936. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  12937. var edge = parentNode.reroutedEdges[childNode.id][i];
  12938. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  12939. edge.originalFromId.pop();
  12940. edge.fromId = childNode.id;
  12941. edge.from = childNode;
  12942. }
  12943. else {
  12944. edge.originalToId.pop();
  12945. edge.toId = childNode.id;
  12946. edge.to = childNode;
  12947. }
  12948. // append this edge to the list of edges connecting to the childnode
  12949. childNode.dynamicEdges.push(edge);
  12950. // remove the edge from the parent object
  12951. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  12952. if (parentNode.dynamicEdges[j].id == edge.id) {
  12953. parentNode.dynamicEdges.splice(j,1);
  12954. break;
  12955. }
  12956. }
  12957. }
  12958. // remove the entry from the rerouted edges
  12959. delete parentNode.reroutedEdges[childNode.id];
  12960. }
  12961. },
  12962. /**
  12963. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  12964. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  12965. * parentNode
  12966. *
  12967. * @param parentNode | Node object
  12968. * @private
  12969. */
  12970. _validateEdges : function(parentNode) {
  12971. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12972. var edge = parentNode.dynamicEdges[i];
  12973. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  12974. parentNode.dynamicEdges.splice(i,1);
  12975. }
  12976. }
  12977. },
  12978. /**
  12979. * This function released the contained edges back into the global domain and puts them back into the
  12980. * dynamic edges of both parent and child.
  12981. *
  12982. * @param {Node} parentNode |
  12983. * @param {Node} childNode |
  12984. * @private
  12985. */
  12986. _releaseContainedEdges : function(parentNode, childNode) {
  12987. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  12988. var edge = parentNode.containedEdges[childNode.id][i];
  12989. // put the edge back in the global edges object
  12990. this.edges[edge.id] = edge;
  12991. // put the edge back in the dynamic edges of the child and parent
  12992. childNode.dynamicEdges.push(edge);
  12993. parentNode.dynamicEdges.push(edge);
  12994. }
  12995. // remove the entry from the contained edges
  12996. delete parentNode.containedEdges[childNode.id];
  12997. },
  12998. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  12999. /**
  13000. * This updates the node labels for all nodes (for debugging purposes)
  13001. */
  13002. updateLabels : function() {
  13003. var nodeId;
  13004. // update node labels
  13005. for (nodeId in this.nodes) {
  13006. if (this.nodes.hasOwnProperty(nodeId)) {
  13007. var node = this.nodes[nodeId];
  13008. if (node.clusterSize > 1) {
  13009. node.label = "[".concat(String(node.clusterSize),"]");
  13010. }
  13011. }
  13012. }
  13013. // update node labels
  13014. for (nodeId in this.nodes) {
  13015. if (this.nodes.hasOwnProperty(nodeId)) {
  13016. node = this.nodes[nodeId];
  13017. if (node.clusterSize == 1) {
  13018. if (node.originalLabel !== undefined) {
  13019. node.label = node.originalLabel;
  13020. }
  13021. else {
  13022. node.label = String(node.id);
  13023. }
  13024. }
  13025. }
  13026. }
  13027. // /* Debug Override */
  13028. // for (nodeId in this.nodes) {
  13029. // if (this.nodes.hasOwnProperty(nodeId)) {
  13030. // node = this.nodes[nodeId];
  13031. // node.label = String(node.level);
  13032. // }
  13033. // }
  13034. },
  13035. /**
  13036. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  13037. * if the rest of the nodes are already a few cluster levels in.
  13038. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  13039. * clustered enough to the clusterToSmallestNeighbours function.
  13040. */
  13041. normalizeClusterLevels : function() {
  13042. var maxLevel = 0;
  13043. var minLevel = 1e9;
  13044. var clusterLevel = 0;
  13045. // we loop over all nodes in the list
  13046. for (var nodeId in this.nodes) {
  13047. if (this.nodes.hasOwnProperty(nodeId)) {
  13048. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  13049. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  13050. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  13051. }
  13052. }
  13053. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  13054. var amountOfNodes = this.nodeIndices.length;
  13055. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  13056. // we loop over all nodes in the list
  13057. for (var nodeId in this.nodes) {
  13058. if (this.nodes.hasOwnProperty(nodeId)) {
  13059. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  13060. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  13061. }
  13062. }
  13063. }
  13064. this._updateNodeIndexList();
  13065. this._updateDynamicEdges();
  13066. // if a cluster was formed, we increase the clusterSession
  13067. if (this.nodeIndices.length != amountOfNodes) {
  13068. this.clusterSession += 1;
  13069. }
  13070. }
  13071. },
  13072. /**
  13073. * This function determines if the cluster we want to decluster is in the active area
  13074. * this means around the zoom center
  13075. *
  13076. * @param {Node} node
  13077. * @returns {boolean}
  13078. * @private
  13079. */
  13080. _nodeInActiveArea : function(node) {
  13081. return (
  13082. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13083. &&
  13084. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13085. )
  13086. },
  13087. /**
  13088. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  13089. * It puts large clusters away from the center and randomizes the order.
  13090. *
  13091. */
  13092. repositionNodes : function() {
  13093. for (var i = 0; i < this.nodeIndices.length; i++) {
  13094. var node = this.nodes[this.nodeIndices[i]];
  13095. if ((node.xFixed == false || node.yFixed == false)) {
  13096. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  13097. var angle = 2 * Math.PI * Math.random();
  13098. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  13099. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  13100. this._repositionBezierNodes(node);
  13101. }
  13102. }
  13103. },
  13104. /**
  13105. * We determine how many connections denote an important hub.
  13106. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  13107. *
  13108. * @private
  13109. */
  13110. _getHubSize : function() {
  13111. var average = 0;
  13112. var averageSquared = 0;
  13113. var hubCounter = 0;
  13114. var largestHub = 0;
  13115. for (var i = 0; i < this.nodeIndices.length; i++) {
  13116. var node = this.nodes[this.nodeIndices[i]];
  13117. if (node.dynamicEdgesLength > largestHub) {
  13118. largestHub = node.dynamicEdgesLength;
  13119. }
  13120. average += node.dynamicEdgesLength;
  13121. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  13122. hubCounter += 1;
  13123. }
  13124. average = average / hubCounter;
  13125. averageSquared = averageSquared / hubCounter;
  13126. var variance = averageSquared - Math.pow(average,2);
  13127. var standardDeviation = Math.sqrt(variance);
  13128. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  13129. // always have at least one to cluster
  13130. if (this.hubThreshold > largestHub) {
  13131. this.hubThreshold = largestHub;
  13132. }
  13133. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  13134. // console.log("hubThreshold:",this.hubThreshold);
  13135. },
  13136. /**
  13137. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  13138. * with this amount we can cluster specifically on these chains.
  13139. *
  13140. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  13141. * @private
  13142. */
  13143. _reduceAmountOfChains : function(fraction) {
  13144. this.hubThreshold = 2;
  13145. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  13146. for (var nodeId in this.nodes) {
  13147. if (this.nodes.hasOwnProperty(nodeId)) {
  13148. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  13149. if (reduceAmount > 0) {
  13150. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  13151. reduceAmount -= 1;
  13152. }
  13153. }
  13154. }
  13155. }
  13156. },
  13157. /**
  13158. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  13159. * with this amount we can cluster specifically on these chains.
  13160. *
  13161. * @private
  13162. */
  13163. _getChainFraction : function() {
  13164. var chains = 0;
  13165. var total = 0;
  13166. for (var nodeId in this.nodes) {
  13167. if (this.nodes.hasOwnProperty(nodeId)) {
  13168. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  13169. chains += 1;
  13170. }
  13171. total += 1;
  13172. }
  13173. }
  13174. return chains/total;
  13175. }
  13176. };
  13177. var SelectionMixin = {
  13178. /**
  13179. * This function can be called from the _doInAllSectors function
  13180. *
  13181. * @param object
  13182. * @param overlappingNodes
  13183. * @private
  13184. */
  13185. _getNodesOverlappingWith : function(object, overlappingNodes) {
  13186. var nodes = this.nodes;
  13187. for (var nodeId in nodes) {
  13188. if (nodes.hasOwnProperty(nodeId)) {
  13189. if (nodes[nodeId].isOverlappingWith(object)) {
  13190. overlappingNodes.push(nodeId);
  13191. }
  13192. }
  13193. }
  13194. },
  13195. /**
  13196. * retrieve all nodes overlapping with given object
  13197. * @param {Object} object An object with parameters left, top, right, bottom
  13198. * @return {Number[]} An array with id's of the overlapping nodes
  13199. * @private
  13200. */
  13201. _getAllNodesOverlappingWith : function (object) {
  13202. var overlappingNodes = [];
  13203. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  13204. return overlappingNodes;
  13205. },
  13206. /**
  13207. * Return a position object in canvasspace from a single point in screenspace
  13208. *
  13209. * @param pointer
  13210. * @returns {{left: number, top: number, right: number, bottom: number}}
  13211. * @private
  13212. */
  13213. _pointerToPositionObject : function(pointer) {
  13214. var x = this._canvasToX(pointer.x);
  13215. var y = this._canvasToY(pointer.y);
  13216. return {left: x,
  13217. top: y,
  13218. right: x,
  13219. bottom: y};
  13220. },
  13221. /**
  13222. * Get the top node at the a specific point (like a click)
  13223. *
  13224. * @param {{x: Number, y: Number}} pointer
  13225. * @return {Node | null} node
  13226. * @private
  13227. */
  13228. _getNodeAt : function (pointer) {
  13229. // we first check if this is an navigation controls element
  13230. var positionObject = this._pointerToPositionObject(pointer);
  13231. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  13232. // if there are overlapping nodes, select the last one, this is the
  13233. // one which is drawn on top of the others
  13234. if (overlappingNodes.length > 0) {
  13235. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  13236. }
  13237. else {
  13238. return null;
  13239. }
  13240. },
  13241. /**
  13242. * retrieve all edges overlapping with given object, selector is around center
  13243. * @param {Object} object An object with parameters left, top, right, bottom
  13244. * @return {Number[]} An array with id's of the overlapping nodes
  13245. * @private
  13246. */
  13247. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  13248. var edges = this.edges;
  13249. for (var edgeId in edges) {
  13250. if (edges.hasOwnProperty(edgeId)) {
  13251. if (edges[edgeId].isOverlappingWith(object)) {
  13252. overlappingEdges.push(edgeId);
  13253. }
  13254. }
  13255. }
  13256. },
  13257. /**
  13258. * retrieve all nodes overlapping with given object
  13259. * @param {Object} object An object with parameters left, top, right, bottom
  13260. * @return {Number[]} An array with id's of the overlapping nodes
  13261. * @private
  13262. */
  13263. _getAllEdgesOverlappingWith : function (object) {
  13264. var overlappingEdges = [];
  13265. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  13266. return overlappingEdges;
  13267. },
  13268. /**
  13269. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  13270. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  13271. *
  13272. * @param pointer
  13273. * @returns {null}
  13274. * @private
  13275. */
  13276. _getEdgeAt : function(pointer) {
  13277. var positionObject = this._pointerToPositionObject(pointer);
  13278. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  13279. if (overlappingEdges.length > 0) {
  13280. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  13281. }
  13282. else {
  13283. return null;
  13284. }
  13285. },
  13286. /**
  13287. * Add object to the selection array.
  13288. *
  13289. * @param obj
  13290. * @private
  13291. */
  13292. _addToSelection : function(obj) {
  13293. if (obj instanceof Node) {
  13294. this.selectionObj.nodes[obj.id] = obj;
  13295. }
  13296. else {
  13297. this.selectionObj.edges[obj.id] = obj;
  13298. }
  13299. },
  13300. /**
  13301. * Remove a single option from selection.
  13302. *
  13303. * @param {Object} obj
  13304. * @private
  13305. */
  13306. _removeFromSelection : function(obj) {
  13307. if (obj instanceof Node) {
  13308. delete this.selectionObj.nodes[obj.id];
  13309. }
  13310. else {
  13311. delete this.selectionObj.edges[obj.id];
  13312. }
  13313. },
  13314. /**
  13315. * Unselect all. The selectionObj is useful for this.
  13316. *
  13317. * @param {Boolean} [doNotTrigger] | ignore trigger
  13318. * @private
  13319. */
  13320. _unselectAll : function(doNotTrigger) {
  13321. if (doNotTrigger === undefined) {
  13322. doNotTrigger = false;
  13323. }
  13324. for(var nodeId in this.selectionObj.nodes) {
  13325. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13326. this.selectionObj.nodes[nodeId].unselect();
  13327. }
  13328. }
  13329. for(var edgeId in this.selectionObj.edges) {
  13330. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13331. this.selectionObj.edges[edgeId].unselect();;
  13332. }
  13333. }
  13334. this.selectionObj = {nodes:{},edges:{}};
  13335. if (doNotTrigger == false) {
  13336. this.emit('select', this.getSelection());
  13337. }
  13338. },
  13339. /**
  13340. * Unselect all clusters. The selectionObj is useful for this.
  13341. *
  13342. * @param {Boolean} [doNotTrigger] | ignore trigger
  13343. * @private
  13344. */
  13345. _unselectClusters : function(doNotTrigger) {
  13346. if (doNotTrigger === undefined) {
  13347. doNotTrigger = false;
  13348. }
  13349. for (var nodeId in this.selectionObj.nodes) {
  13350. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13351. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  13352. this.selectionObj.nodes[nodeId].unselect();
  13353. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  13354. }
  13355. }
  13356. }
  13357. if (doNotTrigger == false) {
  13358. this.emit('select', this.getSelection());
  13359. }
  13360. },
  13361. /**
  13362. * return the number of selected nodes
  13363. *
  13364. * @returns {number}
  13365. * @private
  13366. */
  13367. _getSelectedNodeCount : function() {
  13368. var count = 0;
  13369. for (var nodeId in this.selectionObj.nodes) {
  13370. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13371. count += 1;
  13372. }
  13373. }
  13374. return count;
  13375. },
  13376. /**
  13377. * return the number of selected nodes
  13378. *
  13379. * @returns {number}
  13380. * @private
  13381. */
  13382. _getSelectedNode : function() {
  13383. for (var nodeId in this.selectionObj.nodes) {
  13384. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13385. return this.selectionObj.nodes[nodeId];
  13386. }
  13387. }
  13388. return null;
  13389. },
  13390. /**
  13391. * return the number of selected edges
  13392. *
  13393. * @returns {number}
  13394. * @private
  13395. */
  13396. _getSelectedEdgeCount : function() {
  13397. var count = 0;
  13398. for (var edgeId in this.selectionObj.edges) {
  13399. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13400. count += 1;
  13401. }
  13402. }
  13403. return count;
  13404. },
  13405. /**
  13406. * return the number of selected objects.
  13407. *
  13408. * @returns {number}
  13409. * @private
  13410. */
  13411. _getSelectedObjectCount : function() {
  13412. var count = 0;
  13413. for(var nodeId in this.selectionObj.nodes) {
  13414. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13415. count += 1;
  13416. }
  13417. }
  13418. for(var edgeId in this.selectionObj.edges) {
  13419. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13420. count += 1;
  13421. }
  13422. }
  13423. return count;
  13424. },
  13425. /**
  13426. * Check if anything is selected
  13427. *
  13428. * @returns {boolean}
  13429. * @private
  13430. */
  13431. _selectionIsEmpty : function() {
  13432. for(var nodeId in this.selectionObj.nodes) {
  13433. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13434. return false;
  13435. }
  13436. }
  13437. for(var edgeId in this.selectionObj.edges) {
  13438. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13439. return false;
  13440. }
  13441. }
  13442. return true;
  13443. },
  13444. /**
  13445. * check if one of the selected nodes is a cluster.
  13446. *
  13447. * @returns {boolean}
  13448. * @private
  13449. */
  13450. _clusterInSelection : function() {
  13451. for(var nodeId in this.selectionObj.nodes) {
  13452. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13453. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  13454. return true;
  13455. }
  13456. }
  13457. }
  13458. return false;
  13459. },
  13460. /**
  13461. * select the edges connected to the node that is being selected
  13462. *
  13463. * @param {Node} node
  13464. * @private
  13465. */
  13466. _selectConnectedEdges : function(node) {
  13467. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13468. var edge = node.dynamicEdges[i];
  13469. edge.select();
  13470. this._addToSelection(edge);
  13471. }
  13472. },
  13473. /**
  13474. * unselect the edges connected to the node that is being selected
  13475. *
  13476. * @param {Node} node
  13477. * @private
  13478. */
  13479. _unselectConnectedEdges : function(node) {
  13480. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13481. var edge = node.dynamicEdges[i];
  13482. edge.unselect();
  13483. this._removeFromSelection(edge);
  13484. }
  13485. },
  13486. /**
  13487. * This is called when someone clicks on a node. either select or deselect it.
  13488. * If there is an existing selection and we don't want to append to it, clear the existing selection
  13489. *
  13490. * @param {Node || Edge} object
  13491. * @param {Boolean} append
  13492. * @param {Boolean} [doNotTrigger] | ignore trigger
  13493. * @private
  13494. */
  13495. _selectObject : function(object, append, doNotTrigger) {
  13496. if (doNotTrigger === undefined) {
  13497. doNotTrigger = false;
  13498. }
  13499. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  13500. this._unselectAll(true);
  13501. }
  13502. if (object.selected == false) {
  13503. object.select();
  13504. this._addToSelection(object);
  13505. if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
  13506. this._selectConnectedEdges(object);
  13507. }
  13508. }
  13509. else {
  13510. object.unselect();
  13511. this._removeFromSelection(object);
  13512. }
  13513. if (doNotTrigger == false) {
  13514. this.emit('select', this.getSelection());
  13515. }
  13516. },
  13517. /**
  13518. * handles the selection part of the touch, only for navigation controls elements;
  13519. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  13520. * This is the most responsive solution
  13521. *
  13522. * @param {Object} pointer
  13523. * @private
  13524. */
  13525. _handleTouch : function(pointer) {
  13526. },
  13527. /**
  13528. * handles the selection part of the tap;
  13529. *
  13530. * @param {Object} pointer
  13531. * @private
  13532. */
  13533. _handleTap : function(pointer) {
  13534. var node = this._getNodeAt(pointer);
  13535. if (node != null) {
  13536. this._selectObject(node,false);
  13537. }
  13538. else {
  13539. var edge = this._getEdgeAt(pointer);
  13540. if (edge != null) {
  13541. this._selectObject(edge,false);
  13542. }
  13543. else {
  13544. this._unselectAll();
  13545. }
  13546. }
  13547. this.emit("click", this.getSelection());
  13548. this._redraw();
  13549. },
  13550. /**
  13551. * handles the selection part of the double tap and opens a cluster if needed
  13552. *
  13553. * @param {Object} pointer
  13554. * @private
  13555. */
  13556. _handleDoubleTap : function(pointer) {
  13557. var node = this._getNodeAt(pointer);
  13558. if (node != null && node !== undefined) {
  13559. // we reset the areaCenter here so the opening of the node will occur
  13560. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  13561. "y" : this._canvasToY(pointer.y)};
  13562. this.openCluster(node);
  13563. }
  13564. this.emit("doubleClick", this.getSelection());
  13565. },
  13566. /**
  13567. * Handle the onHold selection part
  13568. *
  13569. * @param pointer
  13570. * @private
  13571. */
  13572. _handleOnHold : function(pointer) {
  13573. var node = this._getNodeAt(pointer);
  13574. if (node != null) {
  13575. this._selectObject(node,true);
  13576. }
  13577. else {
  13578. var edge = this._getEdgeAt(pointer);
  13579. if (edge != null) {
  13580. this._selectObject(edge,true);
  13581. }
  13582. }
  13583. this._redraw();
  13584. },
  13585. /**
  13586. * handle the onRelease event. These functions are here for the navigation controls module.
  13587. *
  13588. * @private
  13589. */
  13590. _handleOnRelease : function(pointer) {
  13591. },
  13592. /**
  13593. *
  13594. * retrieve the currently selected objects
  13595. * @return {Number[] | String[]} selection An array with the ids of the
  13596. * selected nodes.
  13597. */
  13598. getSelection : function() {
  13599. var nodeIds = this.getSelectedNodes();
  13600. var edgeIds = this.getSelectedEdges();
  13601. return {nodes:nodeIds, edges:edgeIds};
  13602. },
  13603. /**
  13604. *
  13605. * retrieve the currently selected nodes
  13606. * @return {String} selection An array with the ids of the
  13607. * selected nodes.
  13608. */
  13609. getSelectedNodes : function() {
  13610. var idArray = [];
  13611. for(var nodeId in this.selectionObj.nodes) {
  13612. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13613. idArray.push(nodeId);
  13614. }
  13615. }
  13616. return idArray
  13617. },
  13618. /**
  13619. *
  13620. * retrieve the currently selected edges
  13621. * @return {Array} selection An array with the ids of the
  13622. * selected nodes.
  13623. */
  13624. getSelectedEdges : function() {
  13625. var idArray = [];
  13626. for(var edgeId in this.selectionObj.edges) {
  13627. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13628. idArray.push(edgeId);
  13629. }
  13630. }
  13631. return idArray;
  13632. },
  13633. /**
  13634. * select zero or more nodes
  13635. * @param {Number[] | String[]} selection An array with the ids of the
  13636. * selected nodes.
  13637. */
  13638. setSelection : function(selection) {
  13639. var i, iMax, id;
  13640. if (!selection || (selection.length == undefined))
  13641. throw 'Selection must be an array with ids';
  13642. // first unselect any selected node
  13643. this._unselectAll(true);
  13644. for (i = 0, iMax = selection.length; i < iMax; i++) {
  13645. id = selection[i];
  13646. var node = this.nodes[id];
  13647. if (!node) {
  13648. throw new RangeError('Node with id "' + id + '" not found');
  13649. }
  13650. this._selectObject(node,true,true);
  13651. }
  13652. this.redraw();
  13653. },
  13654. /**
  13655. * Validate the selection: remove ids of nodes which no longer exist
  13656. * @private
  13657. */
  13658. _updateSelection : function () {
  13659. for(var nodeId in this.selectionObj.nodes) {
  13660. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13661. if (!this.nodes.hasOwnProperty(nodeId)) {
  13662. delete this.selectionObj.nodes[nodeId];
  13663. }
  13664. }
  13665. }
  13666. for(var edgeId in this.selectionObj.edges) {
  13667. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13668. if (!this.edges.hasOwnProperty(edgeId)) {
  13669. delete this.selectionObj.edges[edgeId];
  13670. }
  13671. }
  13672. }
  13673. }
  13674. };
  13675. /**
  13676. * Created by Alex on 1/22/14.
  13677. */
  13678. var NavigationMixin = {
  13679. _cleanNavigation : function() {
  13680. // clean up previosu navigation items
  13681. var wrapper = document.getElementById('graph-navigation_wrapper');
  13682. if (wrapper != null) {
  13683. this.containerElement.removeChild(wrapper);
  13684. }
  13685. document.onmouseup = null;
  13686. },
  13687. /**
  13688. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  13689. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  13690. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  13691. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  13692. *
  13693. * @private
  13694. */
  13695. _loadNavigationElements : function() {
  13696. this._cleanNavigation();
  13697. this.navigationDivs = {};
  13698. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  13699. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  13700. this.navigationDivs['wrapper'] = document.createElement('div');
  13701. this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
  13702. this.navigationDivs['wrapper'].style.position = "absolute";
  13703. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  13704. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  13705. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  13706. for (var i = 0; i < navigationDivs.length; i++) {
  13707. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  13708. this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
  13709. this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
  13710. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  13711. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  13712. }
  13713. document.onmouseup = this._stopMovement.bind(this);
  13714. },
  13715. /**
  13716. * this stops all movement induced by the navigation buttons
  13717. *
  13718. * @private
  13719. */
  13720. _stopMovement : function() {
  13721. this._xStopMoving();
  13722. this._yStopMoving();
  13723. this._stopZoom();
  13724. },
  13725. /**
  13726. * stops the actions performed by page up and down etc.
  13727. *
  13728. * @param event
  13729. * @private
  13730. */
  13731. _preventDefault : function(event) {
  13732. if (event !== undefined) {
  13733. if (event.preventDefault) {
  13734. event.preventDefault();
  13735. } else {
  13736. event.returnValue = false;
  13737. }
  13738. }
  13739. },
  13740. /**
  13741. * move the screen up
  13742. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  13743. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  13744. * To avoid this behaviour, we do the translation in the start loop.
  13745. *
  13746. * @private
  13747. */
  13748. _moveUp : function(event) {
  13749. this.yIncrement = this.constants.keyboard.speed.y;
  13750. this.start(); // if there is no node movement, the calculation wont be done
  13751. this._preventDefault(event);
  13752. if (this.navigationDivs) {
  13753. this.navigationDivs['up'].className += " active";
  13754. }
  13755. },
  13756. /**
  13757. * move the screen down
  13758. * @private
  13759. */
  13760. _moveDown : function(event) {
  13761. this.yIncrement = -this.constants.keyboard.speed.y;
  13762. this.start(); // if there is no node movement, the calculation wont be done
  13763. this._preventDefault(event);
  13764. if (this.navigationDivs) {
  13765. this.navigationDivs['down'].className += " active";
  13766. }
  13767. },
  13768. /**
  13769. * move the screen left
  13770. * @private
  13771. */
  13772. _moveLeft : function(event) {
  13773. this.xIncrement = this.constants.keyboard.speed.x;
  13774. this.start(); // if there is no node movement, the calculation wont be done
  13775. this._preventDefault(event);
  13776. if (this.navigationDivs) {
  13777. this.navigationDivs['left'].className += " active";
  13778. }
  13779. },
  13780. /**
  13781. * move the screen right
  13782. * @private
  13783. */
  13784. _moveRight : function(event) {
  13785. this.xIncrement = -this.constants.keyboard.speed.y;
  13786. this.start(); // if there is no node movement, the calculation wont be done
  13787. this._preventDefault(event);
  13788. if (this.navigationDivs) {
  13789. this.navigationDivs['right'].className += " active";
  13790. }
  13791. },
  13792. /**
  13793. * Zoom in, using the same method as the movement.
  13794. * @private
  13795. */
  13796. _zoomIn : function(event) {
  13797. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  13798. this.start(); // if there is no node movement, the calculation wont be done
  13799. this._preventDefault(event);
  13800. if (this.navigationDivs) {
  13801. this.navigationDivs['zoomIn'].className += " active";
  13802. }
  13803. },
  13804. /**
  13805. * Zoom out
  13806. * @private
  13807. */
  13808. _zoomOut : function() {
  13809. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  13810. this.start(); // if there is no node movement, the calculation wont be done
  13811. this._preventDefault(event);
  13812. if (this.navigationDivs) {
  13813. this.navigationDivs['zoomOut'].className += " active";
  13814. }
  13815. },
  13816. /**
  13817. * Stop zooming and unhighlight the zoom controls
  13818. * @private
  13819. */
  13820. _stopZoom : function() {
  13821. this.zoomIncrement = 0;
  13822. if (this.navigationDivs) {
  13823. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  13824. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  13825. }
  13826. },
  13827. /**
  13828. * Stop moving in the Y direction and unHighlight the up and down
  13829. * @private
  13830. */
  13831. _yStopMoving : function() {
  13832. this.yIncrement = 0;
  13833. if (this.navigationDivs) {
  13834. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  13835. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  13836. }
  13837. },
  13838. /**
  13839. * Stop moving in the X direction and unHighlight left and right.
  13840. * @private
  13841. */
  13842. _xStopMoving : function() {
  13843. this.xIncrement = 0;
  13844. if (this.navigationDivs) {
  13845. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  13846. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  13847. }
  13848. }
  13849. };
  13850. /**
  13851. * Created by Alex on 2/10/14.
  13852. */
  13853. var graphMixinLoaders = {
  13854. /**
  13855. * Load a mixin into the graph object
  13856. *
  13857. * @param {Object} sourceVariable | this object has to contain functions.
  13858. * @private
  13859. */
  13860. _loadMixin: function (sourceVariable) {
  13861. for (var mixinFunction in sourceVariable) {
  13862. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13863. Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
  13864. }
  13865. }
  13866. },
  13867. /**
  13868. * removes a mixin from the graph object.
  13869. *
  13870. * @param {Object} sourceVariable | this object has to contain functions.
  13871. * @private
  13872. */
  13873. _clearMixin: function (sourceVariable) {
  13874. for (var mixinFunction in sourceVariable) {
  13875. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13876. Graph.prototype[mixinFunction] = undefined;
  13877. }
  13878. }
  13879. },
  13880. /**
  13881. * Mixin the physics system and initialize the parameters required.
  13882. *
  13883. * @private
  13884. */
  13885. _loadPhysicsSystem: function () {
  13886. this._loadMixin(physicsMixin);
  13887. this._loadSelectedForceSolver();
  13888. if (this.constants.configurePhysics == true) {
  13889. this._loadPhysicsConfiguration();
  13890. }
  13891. },
  13892. /**
  13893. * Mixin the cluster system and initialize the parameters required.
  13894. *
  13895. * @private
  13896. */
  13897. _loadClusterSystem: function () {
  13898. this.clusterSession = 0;
  13899. this.hubThreshold = 5;
  13900. this._loadMixin(ClusterMixin);
  13901. },
  13902. /**
  13903. * Mixin the sector system and initialize the parameters required
  13904. *
  13905. * @private
  13906. */
  13907. _loadSectorSystem: function () {
  13908. this.sectors = { },
  13909. this.activeSector = ["default"];
  13910. this.sectors["active"] = { },
  13911. this.sectors["active"]["default"] = {"nodes": {},
  13912. "edges": {},
  13913. "nodeIndices": [],
  13914. "formationScale": 1.0,
  13915. "drawingNode": undefined };
  13916. this.sectors["frozen"] = {},
  13917. this.sectors["support"] = {"nodes": {},
  13918. "edges": {},
  13919. "nodeIndices": [],
  13920. "formationScale": 1.0,
  13921. "drawingNode": undefined };
  13922. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  13923. this._loadMixin(SectorMixin);
  13924. },
  13925. /**
  13926. * Mixin the selection system and initialize the parameters required
  13927. *
  13928. * @private
  13929. */
  13930. _loadSelectionSystem: function () {
  13931. this.selectionObj = {nodes: {}, edges: {}};
  13932. this._loadMixin(SelectionMixin);
  13933. },
  13934. /**
  13935. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  13936. *
  13937. * @private
  13938. */
  13939. _loadManipulationSystem: function () {
  13940. // reset global variables -- these are used by the selection of nodes and edges.
  13941. this.blockConnectingEdgeSelection = false;
  13942. this.forceAppendSelection = false
  13943. if (this.constants.dataManipulation.enabled == true) {
  13944. // load the manipulator HTML elements. All styling done in css.
  13945. if (this.manipulationDiv === undefined) {
  13946. this.manipulationDiv = document.createElement('div');
  13947. this.manipulationDiv.className = 'graph-manipulationDiv';
  13948. this.manipulationDiv.id = 'graph-manipulationDiv';
  13949. if (this.editMode == true) {
  13950. this.manipulationDiv.style.display = "block";
  13951. }
  13952. else {
  13953. this.manipulationDiv.style.display = "none";
  13954. }
  13955. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  13956. }
  13957. if (this.editModeDiv === undefined) {
  13958. this.editModeDiv = document.createElement('div');
  13959. this.editModeDiv.className = 'graph-manipulation-editMode';
  13960. this.editModeDiv.id = 'graph-manipulation-editMode';
  13961. if (this.editMode == true) {
  13962. this.editModeDiv.style.display = "none";
  13963. }
  13964. else {
  13965. this.editModeDiv.style.display = "block";
  13966. }
  13967. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  13968. }
  13969. if (this.closeDiv === undefined) {
  13970. this.closeDiv = document.createElement('div');
  13971. this.closeDiv.className = 'graph-manipulation-closeDiv';
  13972. this.closeDiv.id = 'graph-manipulation-closeDiv';
  13973. this.closeDiv.style.display = this.manipulationDiv.style.display;
  13974. this.containerElement.insertBefore(this.closeDiv, this.frame);
  13975. }
  13976. // load the manipulation functions
  13977. this._loadMixin(manipulationMixin);
  13978. // create the manipulator toolbar
  13979. this._createManipulatorBar();
  13980. }
  13981. else {
  13982. if (this.manipulationDiv !== undefined) {
  13983. // removes all the bindings and overloads
  13984. this._createManipulatorBar();
  13985. // remove the manipulation divs
  13986. this.containerElement.removeChild(this.manipulationDiv);
  13987. this.containerElement.removeChild(this.editModeDiv);
  13988. this.containerElement.removeChild(this.closeDiv);
  13989. this.manipulationDiv = undefined;
  13990. this.editModeDiv = undefined;
  13991. this.closeDiv = undefined;
  13992. // remove the mixin functions
  13993. this._clearMixin(manipulationMixin);
  13994. }
  13995. }
  13996. },
  13997. /**
  13998. * Mixin the navigation (User Interface) system and initialize the parameters required
  13999. *
  14000. * @private
  14001. */
  14002. _loadNavigationControls: function () {
  14003. this._loadMixin(NavigationMixin);
  14004. // the clean function removes the button divs, this is done to remove the bindings.
  14005. this._cleanNavigation();
  14006. if (this.constants.navigation.enabled == true) {
  14007. this._loadNavigationElements();
  14008. }
  14009. },
  14010. /**
  14011. * Mixin the hierarchical layout system.
  14012. *
  14013. * @private
  14014. */
  14015. _loadHierarchySystem: function () {
  14016. this._loadMixin(HierarchicalLayoutMixin);
  14017. }
  14018. };
  14019. /**
  14020. * @constructor Graph
  14021. * Create a graph visualization, displaying nodes and edges.
  14022. *
  14023. * @param {Element} container The DOM element in which the Graph will
  14024. * be created. Normally a div element.
  14025. * @param {Object} data An object containing parameters
  14026. * {Array} nodes
  14027. * {Array} edges
  14028. * @param {Object} options Options
  14029. */
  14030. function Graph (container, data, options) {
  14031. this._initializeMixinLoaders();
  14032. // create variables and set default values
  14033. this.containerElement = container;
  14034. this.width = '100%';
  14035. this.height = '100%';
  14036. // render and calculation settings
  14037. this.renderRefreshRate = 60; // hz (fps)
  14038. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  14039. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  14040. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  14041. this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
  14042. this.stabilize = true; // stabilize before displaying the graph
  14043. this.selectable = true;
  14044. this.initializing = true;
  14045. // these functions are triggered when the dataset is edited
  14046. this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
  14047. // set constant values
  14048. this.constants = {
  14049. nodes: {
  14050. radiusMin: 5,
  14051. radiusMax: 20,
  14052. radius: 5,
  14053. shape: 'ellipse',
  14054. image: undefined,
  14055. widthMin: 16, // px
  14056. widthMax: 64, // px
  14057. fixed: false,
  14058. fontColor: 'black',
  14059. fontSize: 14, // px
  14060. fontFace: 'verdana',
  14061. level: -1,
  14062. color: {
  14063. border: '#2B7CE9',
  14064. background: '#97C2FC',
  14065. highlight: {
  14066. border: '#2B7CE9',
  14067. background: '#D2E5FF'
  14068. }
  14069. },
  14070. borderColor: '#2B7CE9',
  14071. backgroundColor: '#97C2FC',
  14072. highlightColor: '#D2E5FF',
  14073. group: undefined
  14074. },
  14075. edges: {
  14076. widthMin: 1,
  14077. widthMax: 15,
  14078. width: 1,
  14079. style: 'line',
  14080. color: {
  14081. color:'#848484',
  14082. highlight:'#848484'
  14083. },
  14084. fontColor: '#343434',
  14085. fontSize: 14, // px
  14086. fontFace: 'arial',
  14087. fontFill: 'white',
  14088. dash: {
  14089. length: 10,
  14090. gap: 5,
  14091. altLength: undefined
  14092. }
  14093. },
  14094. configurePhysics:false,
  14095. physics: {
  14096. barnesHut: {
  14097. enabled: true,
  14098. theta: 1 / 0.6, // inverted to save time during calculation
  14099. gravitationalConstant: -2000,
  14100. centralGravity: 0.3,
  14101. springLength: 95,
  14102. springConstant: 0.04,
  14103. damping: 0.09
  14104. },
  14105. repulsion: {
  14106. centralGravity: 0.1,
  14107. springLength: 200,
  14108. springConstant: 0.05,
  14109. nodeDistance: 100,
  14110. damping: 0.09
  14111. },
  14112. hierarchicalRepulsion: {
  14113. enabled: false,
  14114. centralGravity: 0.0,
  14115. springLength: 100,
  14116. springConstant: 0.01,
  14117. nodeDistance: 60,
  14118. damping: 0.09
  14119. },
  14120. damping: null,
  14121. centralGravity: null,
  14122. springLength: null,
  14123. springConstant: null
  14124. },
  14125. clustering: { // Per Node in Cluster = PNiC
  14126. enabled: false, // (Boolean) | global on/off switch for clustering.
  14127. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  14128. 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
  14129. 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
  14130. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  14131. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  14132. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  14133. 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.
  14134. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  14135. maxFontSize: 1000,
  14136. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  14137. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  14138. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  14139. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  14140. height: 1, // (px PNiC) | growth of the height per node in cluster.
  14141. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  14142. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  14143. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  14144. clusterLevelDifference: 2
  14145. },
  14146. navigation: {
  14147. enabled: false
  14148. },
  14149. keyboard: {
  14150. enabled: false,
  14151. speed: {x: 10, y: 10, zoom: 0.02}
  14152. },
  14153. dataManipulation: {
  14154. enabled: false,
  14155. initiallyVisible: false
  14156. },
  14157. hierarchicalLayout: {
  14158. enabled:false,
  14159. levelSeparation: 150,
  14160. nodeSpacing: 100,
  14161. direction: "UD" // UD, DU, LR, RL
  14162. },
  14163. freezeForStabilization: false,
  14164. smoothCurves: true,
  14165. maxVelocity: 10,
  14166. minVelocity: 0.1, // px/s
  14167. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  14168. labels:{
  14169. add:"Add Node",
  14170. edit:"Edit",
  14171. link:"Add Link",
  14172. del:"Delete selected",
  14173. editNode:"Edit Node",
  14174. back:"Back",
  14175. addDescription:"Click in an empty space to place a new node.",
  14176. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  14177. addError:"The function for add does not support two arguments (data,callback).",
  14178. linkError:"The function for connect does not support two arguments (data,callback).",
  14179. editError:"The function for edit does not support two arguments (data, callback).",
  14180. editBoundError:"No edit function has been bound to this button.",
  14181. deleteError:"The function for delete does not support two arguments (data, callback).",
  14182. deleteClusterError:"Clusters cannot be deleted."
  14183. },
  14184. tooltip: {
  14185. delay: 300,
  14186. fontColor: 'black',
  14187. fontSize: 14, // px
  14188. fontFace: 'verdana',
  14189. color: {
  14190. border: '#666',
  14191. background: '#FFFFC6'
  14192. }
  14193. }
  14194. };
  14195. this.editMode = this.constants.dataManipulation.initiallyVisible;
  14196. // Node variables
  14197. var graph = this;
  14198. this.groups = new Groups(); // object with groups
  14199. this.images = new Images(); // object with images
  14200. this.images.setOnloadCallback(function () {
  14201. graph._redraw();
  14202. });
  14203. // keyboard navigation variables
  14204. this.xIncrement = 0;
  14205. this.yIncrement = 0;
  14206. this.zoomIncrement = 0;
  14207. // loading all the mixins:
  14208. // load the force calculation functions, grouped under the physics system.
  14209. this._loadPhysicsSystem();
  14210. // create a frame and canvas
  14211. this._create();
  14212. // load the sector system. (mandatory, fully integrated with Graph)
  14213. this._loadSectorSystem();
  14214. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  14215. this._loadClusterSystem();
  14216. // load the selection system. (mandatory, required by Graph)
  14217. this._loadSelectionSystem();
  14218. // load the selection system. (mandatory, required by Graph)
  14219. this._loadHierarchySystem();
  14220. // apply options
  14221. this.setOptions(options);
  14222. // other vars
  14223. this.freezeSimulation = false;// freeze the simulation
  14224. this.cachedFunctions = {};
  14225. // containers for nodes and edges
  14226. this.calculationNodes = {};
  14227. this.calculationNodeIndices = [];
  14228. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  14229. this.nodes = {}; // object with Node objects
  14230. this.edges = {}; // object with Edge objects
  14231. // position and scale variables and objects
  14232. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  14233. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14234. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14235. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  14236. this.scale = 1; // defining the global scale variable in the constructor
  14237. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  14238. // datasets or dataviews
  14239. this.nodesData = null; // A DataSet or DataView
  14240. this.edgesData = null; // A DataSet or DataView
  14241. // create event listeners used to subscribe on the DataSets of the nodes and edges
  14242. this.nodesListeners = {
  14243. 'add': function (event, params) {
  14244. graph._addNodes(params.items);
  14245. graph.start();
  14246. },
  14247. 'update': function (event, params) {
  14248. graph._updateNodes(params.items);
  14249. graph.start();
  14250. },
  14251. 'remove': function (event, params) {
  14252. graph._removeNodes(params.items);
  14253. graph.start();
  14254. }
  14255. };
  14256. this.edgesListeners = {
  14257. 'add': function (event, params) {
  14258. graph._addEdges(params.items);
  14259. graph.start();
  14260. },
  14261. 'update': function (event, params) {
  14262. graph._updateEdges(params.items);
  14263. graph.start();
  14264. },
  14265. 'remove': function (event, params) {
  14266. graph._removeEdges(params.items);
  14267. graph.start();
  14268. }
  14269. };
  14270. // properties for the animation
  14271. this.moving = true;
  14272. this.timer = undefined; // Scheduling function. Is definded in this.start();
  14273. // load data (the disable start variable will be the same as the enabled clustering)
  14274. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  14275. // hierarchical layout
  14276. this.initializing = false;
  14277. if (this.constants.hierarchicalLayout.enabled == true) {
  14278. this._setupHierarchicalLayout();
  14279. }
  14280. else {
  14281. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  14282. if (this.stabilize == false) {
  14283. this.zoomExtent(true,this.constants.clustering.enabled);
  14284. }
  14285. }
  14286. // if clustering is disabled, the simulation will have started in the setData function
  14287. if (this.constants.clustering.enabled) {
  14288. this.startWithClustering();
  14289. }
  14290. }
  14291. // Extend Graph with an Emitter mixin
  14292. Emitter(Graph.prototype);
  14293. /**
  14294. * Get the script path where the vis.js library is located
  14295. *
  14296. * @returns {string | null} path Path or null when not found. Path does not
  14297. * end with a slash.
  14298. * @private
  14299. */
  14300. Graph.prototype._getScriptPath = function() {
  14301. var scripts = document.getElementsByTagName( 'script' );
  14302. // find script named vis.js or vis.min.js
  14303. for (var i = 0; i < scripts.length; i++) {
  14304. var src = scripts[i].src;
  14305. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  14306. if (match) {
  14307. // return path without the script name
  14308. return src.substring(0, src.length - match[0].length);
  14309. }
  14310. }
  14311. return null;
  14312. };
  14313. /**
  14314. * Find the center position of the graph
  14315. * @private
  14316. */
  14317. Graph.prototype._getRange = function() {
  14318. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  14319. for (var nodeId in this.nodes) {
  14320. if (this.nodes.hasOwnProperty(nodeId)) {
  14321. node = this.nodes[nodeId];
  14322. if (minX > (node.x)) {minX = node.x;}
  14323. if (maxX < (node.x)) {maxX = node.x;}
  14324. if (minY > (node.y)) {minY = node.y;}
  14325. if (maxY < (node.y)) {maxY = node.y;}
  14326. }
  14327. }
  14328. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  14329. minY = 0, maxY = 0, minX = 0, maxX = 0;
  14330. }
  14331. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14332. };
  14333. /**
  14334. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14335. * @returns {{x: number, y: number}}
  14336. * @private
  14337. */
  14338. Graph.prototype._findCenter = function(range) {
  14339. return {x: (0.5 * (range.maxX + range.minX)),
  14340. y: (0.5 * (range.maxY + range.minY))};
  14341. };
  14342. /**
  14343. * center the graph
  14344. *
  14345. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14346. */
  14347. Graph.prototype._centerGraph = function(range) {
  14348. var center = this._findCenter(range);
  14349. center.x *= this.scale;
  14350. center.y *= this.scale;
  14351. center.x -= 0.5 * this.frame.canvas.clientWidth;
  14352. center.y -= 0.5 * this.frame.canvas.clientHeight;
  14353. this._setTranslation(-center.x,-center.y); // set at 0,0
  14354. };
  14355. /**
  14356. * This function zooms out to fit all data on screen based on amount of nodes
  14357. *
  14358. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  14359. */
  14360. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  14361. if (initialZoom === undefined) {
  14362. initialZoom = false;
  14363. }
  14364. if (disableStart === undefined) {
  14365. disableStart = false;
  14366. }
  14367. var range = this._getRange();
  14368. var zoomLevel;
  14369. if (initialZoom == true) {
  14370. var numberOfNodes = this.nodeIndices.length;
  14371. if (this.constants.smoothCurves == true) {
  14372. if (this.constants.clustering.enabled == true &&
  14373. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14374. 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.
  14375. }
  14376. else {
  14377. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14378. }
  14379. }
  14380. else {
  14381. if (this.constants.clustering.enabled == true &&
  14382. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14383. 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.
  14384. }
  14385. else {
  14386. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14387. }
  14388. }
  14389. // correct for larger canvasses.
  14390. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  14391. zoomLevel *= factor;
  14392. }
  14393. else {
  14394. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  14395. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  14396. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  14397. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  14398. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  14399. }
  14400. if (zoomLevel > 1.0) {
  14401. zoomLevel = 1.0;
  14402. }
  14403. this._setScale(zoomLevel);
  14404. this._centerGraph(range);
  14405. if (disableStart == false) {
  14406. this.moving = true;
  14407. this.start();
  14408. }
  14409. };
  14410. /**
  14411. * Update the this.nodeIndices with the most recent node index list
  14412. * @private
  14413. */
  14414. Graph.prototype._updateNodeIndexList = function() {
  14415. this._clearNodeIndexList();
  14416. for (var idx in this.nodes) {
  14417. if (this.nodes.hasOwnProperty(idx)) {
  14418. this.nodeIndices.push(idx);
  14419. }
  14420. }
  14421. };
  14422. /**
  14423. * Set nodes and edges, and optionally options as well.
  14424. *
  14425. * @param {Object} data Object containing parameters:
  14426. * {Array | DataSet | DataView} [nodes] Array with nodes
  14427. * {Array | DataSet | DataView} [edges] Array with edges
  14428. * {String} [dot] String containing data in DOT format
  14429. * {Options} [options] Object with options
  14430. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  14431. */
  14432. Graph.prototype.setData = function(data, disableStart) {
  14433. if (disableStart === undefined) {
  14434. disableStart = false;
  14435. }
  14436. if (data && data.dot && (data.nodes || data.edges)) {
  14437. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  14438. ' parameter pair "nodes" and "edges", but not both.');
  14439. }
  14440. // set options
  14441. this.setOptions(data && data.options);
  14442. // set all data
  14443. if (data && data.dot) {
  14444. // parse DOT file
  14445. if(data && data.dot) {
  14446. var dotData = vis.util.DOTToGraph(data.dot);
  14447. this.setData(dotData);
  14448. return;
  14449. }
  14450. }
  14451. else {
  14452. this._setNodes(data && data.nodes);
  14453. this._setEdges(data && data.edges);
  14454. }
  14455. this._putDataInSector();
  14456. if (!disableStart) {
  14457. // find a stable position or start animating to a stable position
  14458. if (this.stabilize) {
  14459. this._stabilize();
  14460. }
  14461. this.start();
  14462. }
  14463. };
  14464. /**
  14465. * Set options
  14466. * @param {Object} options
  14467. */
  14468. Graph.prototype.setOptions = function (options) {
  14469. if (options) {
  14470. var prop;
  14471. // retrieve parameter values
  14472. if (options.width !== undefined) {this.width = options.width;}
  14473. if (options.height !== undefined) {this.height = options.height;}
  14474. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  14475. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  14476. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  14477. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  14478. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  14479. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  14480. if (options.labels !== undefined) {
  14481. for (prop in options.labels) {
  14482. if (options.labels.hasOwnProperty(prop)) {
  14483. this.constants.labels[prop] = options.labels[prop];
  14484. }
  14485. }
  14486. }
  14487. if (options.onAdd) {
  14488. this.triggerFunctions.add = options.onAdd;
  14489. }
  14490. if (options.onEdit) {
  14491. this.triggerFunctions.edit = options.onEdit;
  14492. }
  14493. if (options.onConnect) {
  14494. this.triggerFunctions.connect = options.onConnect;
  14495. }
  14496. if (options.onDelete) {
  14497. this.triggerFunctions.del = options.onDelete;
  14498. }
  14499. if (options.physics) {
  14500. if (options.physics.barnesHut) {
  14501. this.constants.physics.barnesHut.enabled = true;
  14502. for (prop in options.physics.barnesHut) {
  14503. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  14504. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  14505. }
  14506. }
  14507. }
  14508. if (options.physics.repulsion) {
  14509. this.constants.physics.barnesHut.enabled = false;
  14510. for (prop in options.physics.repulsion) {
  14511. if (options.physics.repulsion.hasOwnProperty(prop)) {
  14512. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  14513. }
  14514. }
  14515. }
  14516. }
  14517. if (options.hierarchicalLayout) {
  14518. this.constants.hierarchicalLayout.enabled = true;
  14519. for (prop in options.hierarchicalLayout) {
  14520. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  14521. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  14522. }
  14523. }
  14524. }
  14525. else if (options.hierarchicalLayout !== undefined) {
  14526. this.constants.hierarchicalLayout.enabled = false;
  14527. }
  14528. if (options.clustering) {
  14529. this.constants.clustering.enabled = true;
  14530. for (prop in options.clustering) {
  14531. if (options.clustering.hasOwnProperty(prop)) {
  14532. this.constants.clustering[prop] = options.clustering[prop];
  14533. }
  14534. }
  14535. }
  14536. else if (options.clustering !== undefined) {
  14537. this.constants.clustering.enabled = false;
  14538. }
  14539. if (options.navigation) {
  14540. this.constants.navigation.enabled = true;
  14541. for (prop in options.navigation) {
  14542. if (options.navigation.hasOwnProperty(prop)) {
  14543. this.constants.navigation[prop] = options.navigation[prop];
  14544. }
  14545. }
  14546. }
  14547. else if (options.navigation !== undefined) {
  14548. this.constants.navigation.enabled = false;
  14549. }
  14550. if (options.keyboard) {
  14551. this.constants.keyboard.enabled = true;
  14552. for (prop in options.keyboard) {
  14553. if (options.keyboard.hasOwnProperty(prop)) {
  14554. this.constants.keyboard[prop] = options.keyboard[prop];
  14555. }
  14556. }
  14557. }
  14558. else if (options.keyboard !== undefined) {
  14559. this.constants.keyboard.enabled = false;
  14560. }
  14561. if (options.dataManipulation) {
  14562. this.constants.dataManipulation.enabled = true;
  14563. for (prop in options.dataManipulation) {
  14564. if (options.dataManipulation.hasOwnProperty(prop)) {
  14565. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  14566. }
  14567. }
  14568. }
  14569. else if (options.dataManipulation !== undefined) {
  14570. this.constants.dataManipulation.enabled = false;
  14571. }
  14572. // TODO: work out these options and document them
  14573. if (options.edges) {
  14574. for (prop in options.edges) {
  14575. if (options.edges.hasOwnProperty(prop)) {
  14576. if (typeof options.edges[prop] != "object") {
  14577. this.constants.edges[prop] = options.edges[prop];
  14578. }
  14579. }
  14580. }
  14581. if (options.edges.color !== undefined) {
  14582. if (util.isString(options.edges.color)) {
  14583. this.constants.edges.color = {};
  14584. this.constants.edges.color.color = options.edges.color;
  14585. this.constants.edges.color.highlight = options.edges.color;
  14586. }
  14587. else {
  14588. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  14589. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  14590. }
  14591. }
  14592. if (!options.edges.fontColor) {
  14593. if (options.edges.color !== undefined) {
  14594. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  14595. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  14596. }
  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 = util.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. if (options.tooltip) {
  14636. for (prop in options.tooltip) {
  14637. if (options.tooltip.hasOwnProperty(prop)) {
  14638. this.constants.tooltip[prop] = options.tooltip[prop];
  14639. }
  14640. }
  14641. if (options.tooltip.color) {
  14642. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  14643. }
  14644. }
  14645. }
  14646. // (Re)loading the mixins that can be enabled or disabled in the options.
  14647. // load the force calculation functions, grouped under the physics system.
  14648. this._loadPhysicsSystem();
  14649. // load the navigation system.
  14650. this._loadNavigationControls();
  14651. // load the data manipulation system
  14652. this._loadManipulationSystem();
  14653. // configure the smooth curves
  14654. this._configureSmoothCurves();
  14655. // bind keys. If disabled, this will not do anything;
  14656. this._createKeyBinds();
  14657. this.setSize(this.width, this.height);
  14658. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  14659. this._setScale(1);
  14660. this._redraw();
  14661. };
  14662. /**
  14663. * Create the main frame for the Graph.
  14664. * This function is executed once when a Graph object is created. The frame
  14665. * contains a canvas, and this canvas contains all objects like the axis and
  14666. * nodes.
  14667. * @private
  14668. */
  14669. Graph.prototype._create = function () {
  14670. // remove all elements from the container element.
  14671. while (this.containerElement.hasChildNodes()) {
  14672. this.containerElement.removeChild(this.containerElement.firstChild);
  14673. }
  14674. this.frame = document.createElement('div');
  14675. this.frame.className = 'graph-frame';
  14676. this.frame.style.position = 'relative';
  14677. this.frame.style.overflow = 'hidden';
  14678. // create the graph canvas (HTML canvas element)
  14679. this.frame.canvas = document.createElement( 'canvas' );
  14680. this.frame.canvas.style.position = 'relative';
  14681. this.frame.appendChild(this.frame.canvas);
  14682. if (!this.frame.canvas.getContext) {
  14683. var noCanvas = document.createElement( 'DIV' );
  14684. noCanvas.style.color = 'red';
  14685. noCanvas.style.fontWeight = 'bold' ;
  14686. noCanvas.style.padding = '10px';
  14687. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14688. this.frame.canvas.appendChild(noCanvas);
  14689. }
  14690. var me = this;
  14691. this.drag = {};
  14692. this.pinch = {};
  14693. this.hammer = Hammer(this.frame.canvas, {
  14694. prevent_default: true
  14695. });
  14696. this.hammer.on('tap', me._onTap.bind(me) );
  14697. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  14698. this.hammer.on('hold', me._onHold.bind(me) );
  14699. this.hammer.on('pinch', me._onPinch.bind(me) );
  14700. this.hammer.on('touch', me._onTouch.bind(me) );
  14701. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14702. this.hammer.on('drag', me._onDrag.bind(me) );
  14703. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14704. this.hammer.on('release', me._onRelease.bind(me) );
  14705. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14706. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14707. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14708. // add the frame to the container element
  14709. this.containerElement.appendChild(this.frame);
  14710. };
  14711. /**
  14712. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  14713. * @private
  14714. */
  14715. Graph.prototype._createKeyBinds = function() {
  14716. var me = this;
  14717. this.mousetrap = mousetrap;
  14718. this.mousetrap.reset();
  14719. if (this.constants.keyboard.enabled == true) {
  14720. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  14721. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  14722. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  14723. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  14724. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  14725. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  14726. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  14727. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  14728. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  14729. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  14730. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  14731. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  14732. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  14733. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  14734. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  14735. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  14736. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  14737. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  14738. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  14739. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  14740. }
  14741. if (this.constants.dataManipulation.enabled == true) {
  14742. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  14743. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  14744. }
  14745. };
  14746. /**
  14747. * Get the pointer location from a touch location
  14748. * @param {{pageX: Number, pageY: Number}} touch
  14749. * @return {{x: Number, y: Number}} pointer
  14750. * @private
  14751. */
  14752. Graph.prototype._getPointer = function (touch) {
  14753. return {
  14754. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14755. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14756. };
  14757. };
  14758. /**
  14759. * On start of a touch gesture, store the pointer
  14760. * @param event
  14761. * @private
  14762. */
  14763. Graph.prototype._onTouch = function (event) {
  14764. this.drag.pointer = this._getPointer(event.gesture.center);
  14765. this.drag.pinched = false;
  14766. this.pinch.scale = this._getScale();
  14767. this._handleTouch(this.drag.pointer);
  14768. };
  14769. /**
  14770. * handle drag start event
  14771. * @private
  14772. */
  14773. Graph.prototype._onDragStart = function () {
  14774. this._handleDragStart();
  14775. };
  14776. /**
  14777. * This function is called by _onDragStart.
  14778. * It is separated out because we can then overload it for the datamanipulation system.
  14779. *
  14780. * @private
  14781. */
  14782. Graph.prototype._handleDragStart = function() {
  14783. var drag = this.drag;
  14784. var node = this._getNodeAt(drag.pointer);
  14785. // note: drag.pointer is set in _onTouch to get the initial touch location
  14786. drag.dragging = true;
  14787. drag.selection = [];
  14788. drag.translation = this._getTranslation();
  14789. drag.nodeId = null;
  14790. if (node != null) {
  14791. drag.nodeId = node.id;
  14792. // select the clicked node if not yet selected
  14793. if (!node.isSelected()) {
  14794. this._selectObject(node,false);
  14795. }
  14796. // create an array with the selected nodes and their original location and status
  14797. for (var objectId in this.selectionObj.nodes) {
  14798. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  14799. var object = this.selectionObj.nodes[objectId];
  14800. var s = {
  14801. id: object.id,
  14802. node: object,
  14803. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14804. x: object.x,
  14805. y: object.y,
  14806. xFixed: object.xFixed,
  14807. yFixed: object.yFixed
  14808. };
  14809. object.xFixed = true;
  14810. object.yFixed = true;
  14811. drag.selection.push(s);
  14812. }
  14813. }
  14814. }
  14815. };
  14816. /**
  14817. * handle drag event
  14818. * @private
  14819. */
  14820. Graph.prototype._onDrag = function (event) {
  14821. this._handleOnDrag(event)
  14822. };
  14823. /**
  14824. * This function is called by _onDrag.
  14825. * It is separated out because we can then overload it for the datamanipulation system.
  14826. *
  14827. * @private
  14828. */
  14829. Graph.prototype._handleOnDrag = function(event) {
  14830. if (this.drag.pinched) {
  14831. return;
  14832. }
  14833. var pointer = this._getPointer(event.gesture.center);
  14834. var me = this,
  14835. drag = this.drag,
  14836. selection = drag.selection;
  14837. if (selection && selection.length) {
  14838. // calculate delta's and new location
  14839. var deltaX = pointer.x - drag.pointer.x,
  14840. deltaY = pointer.y - drag.pointer.y;
  14841. // update position of all selected nodes
  14842. selection.forEach(function (s) {
  14843. var node = s.node;
  14844. if (!s.xFixed) {
  14845. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  14846. }
  14847. if (!s.yFixed) {
  14848. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  14849. }
  14850. });
  14851. // start _animationStep if not yet running
  14852. if (!this.moving) {
  14853. this.moving = true;
  14854. this.start();
  14855. }
  14856. }
  14857. else {
  14858. // move the graph
  14859. var diffX = pointer.x - this.drag.pointer.x;
  14860. var diffY = pointer.y - this.drag.pointer.y;
  14861. this._setTranslation(
  14862. this.drag.translation.x + diffX,
  14863. this.drag.translation.y + diffY);
  14864. this._redraw();
  14865. this.moved = true;
  14866. }
  14867. };
  14868. /**
  14869. * handle drag start event
  14870. * @private
  14871. */
  14872. Graph.prototype._onDragEnd = function () {
  14873. this.drag.dragging = false;
  14874. var selection = this.drag.selection;
  14875. if (selection) {
  14876. selection.forEach(function (s) {
  14877. // restore original xFixed and yFixed
  14878. s.node.xFixed = s.xFixed;
  14879. s.node.yFixed = s.yFixed;
  14880. });
  14881. }
  14882. };
  14883. /**
  14884. * handle tap/click event: select/unselect a node
  14885. * @private
  14886. */
  14887. Graph.prototype._onTap = function (event) {
  14888. var pointer = this._getPointer(event.gesture.center);
  14889. this.pointerPosition = pointer;
  14890. this._handleTap(pointer);
  14891. };
  14892. /**
  14893. * handle doubletap event
  14894. * @private
  14895. */
  14896. Graph.prototype._onDoubleTap = function (event) {
  14897. var pointer = this._getPointer(event.gesture.center);
  14898. this._handleDoubleTap(pointer);
  14899. };
  14900. /**
  14901. * handle long tap event: multi select nodes
  14902. * @private
  14903. */
  14904. Graph.prototype._onHold = function (event) {
  14905. var pointer = this._getPointer(event.gesture.center);
  14906. this.pointerPosition = pointer;
  14907. this._handleOnHold(pointer);
  14908. };
  14909. /**
  14910. * handle the release of the screen
  14911. *
  14912. * @private
  14913. */
  14914. Graph.prototype._onRelease = function (event) {
  14915. var pointer = this._getPointer(event.gesture.center);
  14916. this._handleOnRelease(pointer);
  14917. };
  14918. /**
  14919. * Handle pinch event
  14920. * @param event
  14921. * @private
  14922. */
  14923. Graph.prototype._onPinch = function (event) {
  14924. var pointer = this._getPointer(event.gesture.center);
  14925. this.drag.pinched = true;
  14926. if (!('scale' in this.pinch)) {
  14927. this.pinch.scale = 1;
  14928. }
  14929. // TODO: enabled moving while pinching?
  14930. var scale = this.pinch.scale * event.gesture.scale;
  14931. this._zoom(scale, pointer)
  14932. };
  14933. /**
  14934. * Zoom the graph in or out
  14935. * @param {Number} scale a number around 1, and between 0.01 and 10
  14936. * @param {{x: Number, y: Number}} pointer Position on screen
  14937. * @return {Number} appliedScale scale is limited within the boundaries
  14938. * @private
  14939. */
  14940. Graph.prototype._zoom = function(scale, pointer) {
  14941. var scaleOld = this._getScale();
  14942. if (scale < 0.00001) {
  14943. scale = 0.00001;
  14944. }
  14945. if (scale > 10) {
  14946. scale = 10;
  14947. }
  14948. // + this.frame.canvas.clientHeight / 2
  14949. var translation = this._getTranslation();
  14950. var scaleFrac = scale / scaleOld;
  14951. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14952. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14953. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  14954. "y" : this._canvasToY(pointer.y)};
  14955. this._setScale(scale);
  14956. this._setTranslation(tx, ty);
  14957. this.updateClustersDefault();
  14958. this._redraw();
  14959. return scale;
  14960. };
  14961. /**
  14962. * Event handler for mouse wheel event, used to zoom the timeline
  14963. * See http://adomas.org/javascript-mouse-wheel/
  14964. * https://github.com/EightMedia/hammer.js/issues/256
  14965. * @param {MouseEvent} event
  14966. * @private
  14967. */
  14968. Graph.prototype._onMouseWheel = function(event) {
  14969. // retrieve delta
  14970. var delta = 0;
  14971. if (event.wheelDelta) { /* IE/Opera. */
  14972. delta = event.wheelDelta/120;
  14973. } else if (event.detail) { /* Mozilla case. */
  14974. // In Mozilla, sign of delta is different than in IE.
  14975. // Also, delta is multiple of 3.
  14976. delta = -event.detail/3;
  14977. }
  14978. // If delta is nonzero, handle it.
  14979. // Basically, delta is now positive if wheel was scrolled up,
  14980. // and negative, if wheel was scrolled down.
  14981. if (delta) {
  14982. // calculate the new scale
  14983. var scale = this._getScale();
  14984. var zoom = delta / 10;
  14985. if (delta < 0) {
  14986. zoom = zoom / (1 - zoom);
  14987. }
  14988. scale *= (1 + zoom);
  14989. // calculate the pointer location
  14990. var gesture = util.fakeGesture(this, event);
  14991. var pointer = this._getPointer(gesture.center);
  14992. // apply the new scale
  14993. this._zoom(scale, pointer);
  14994. }
  14995. // Prevent default actions caused by mouse wheel.
  14996. event.preventDefault();
  14997. };
  14998. /**
  14999. * Mouse move handler for checking whether the title moves over a node with a title.
  15000. * @param {Event} event
  15001. * @private
  15002. */
  15003. Graph.prototype._onMouseMoveTitle = function (event) {
  15004. var gesture = util.fakeGesture(this, event);
  15005. var pointer = this._getPointer(gesture.center);
  15006. // check if the previously selected node is still selected
  15007. if (this.popupNode) {
  15008. this._checkHidePopup(pointer);
  15009. }
  15010. // start a timeout that will check if the mouse is positioned above
  15011. // an element
  15012. var me = this;
  15013. var checkShow = function() {
  15014. me._checkShowPopup(pointer);
  15015. };
  15016. if (this.popupTimer) {
  15017. clearInterval(this.popupTimer); // stop any running calculationTimer
  15018. }
  15019. if (!this.drag.dragging) {
  15020. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  15021. }
  15022. };
  15023. /**
  15024. * Check if there is an element on the given position in the graph
  15025. * (a node or edge). If so, and if this element has a title,
  15026. * show a popup window with its title.
  15027. *
  15028. * @param {{x:Number, y:Number}} pointer
  15029. * @private
  15030. */
  15031. Graph.prototype._checkShowPopup = function (pointer) {
  15032. var obj = {
  15033. left: this._canvasToX(pointer.x),
  15034. top: this._canvasToY(pointer.y),
  15035. right: this._canvasToX(pointer.x),
  15036. bottom: this._canvasToY(pointer.y)
  15037. };
  15038. var id;
  15039. var lastPopupNode = this.popupNode;
  15040. if (this.popupNode == undefined) {
  15041. // search the nodes for overlap, select the top one in case of multiple nodes
  15042. var nodes = this.nodes;
  15043. for (id in nodes) {
  15044. if (nodes.hasOwnProperty(id)) {
  15045. var node = nodes[id];
  15046. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  15047. this.popupNode = node;
  15048. break;
  15049. }
  15050. }
  15051. }
  15052. }
  15053. if (this.popupNode === undefined) {
  15054. // search the edges for overlap
  15055. var edges = this.edges;
  15056. for (id in edges) {
  15057. if (edges.hasOwnProperty(id)) {
  15058. var edge = edges[id];
  15059. if (edge.connected && (edge.getTitle() !== undefined) &&
  15060. edge.isOverlappingWith(obj)) {
  15061. this.popupNode = edge;
  15062. break;
  15063. }
  15064. }
  15065. }
  15066. }
  15067. if (this.popupNode) {
  15068. // show popup message window
  15069. if (this.popupNode != lastPopupNode) {
  15070. var me = this;
  15071. if (!me.popup) {
  15072. me.popup = new Popup(me.frame, me.constants.tooltip);
  15073. }
  15074. // adjust a small offset such that the mouse cursor is located in the
  15075. // bottom left location of the popup, and you can easily move over the
  15076. // popup area
  15077. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  15078. me.popup.setText(me.popupNode.getTitle());
  15079. me.popup.show();
  15080. }
  15081. }
  15082. else {
  15083. if (this.popup) {
  15084. this.popup.hide();
  15085. }
  15086. }
  15087. };
  15088. /**
  15089. * Check if the popup must be hided, which is the case when the mouse is no
  15090. * longer hovering on the object
  15091. * @param {{x:Number, y:Number}} pointer
  15092. * @private
  15093. */
  15094. Graph.prototype._checkHidePopup = function (pointer) {
  15095. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  15096. this.popupNode = undefined;
  15097. if (this.popup) {
  15098. this.popup.hide();
  15099. }
  15100. }
  15101. };
  15102. /**
  15103. * Set a new size for the graph
  15104. * @param {string} width Width in pixels or percentage (for example '800px'
  15105. * or '50%')
  15106. * @param {string} height Height in pixels or percentage (for example '400px'
  15107. * or '30%')
  15108. */
  15109. Graph.prototype.setSize = function(width, height) {
  15110. this.frame.style.width = width;
  15111. this.frame.style.height = height;
  15112. this.frame.canvas.style.width = '100%';
  15113. this.frame.canvas.style.height = '100%';
  15114. this.frame.canvas.width = this.frame.canvas.clientWidth;
  15115. this.frame.canvas.height = this.frame.canvas.clientHeight;
  15116. if (this.manipulationDiv !== undefined) {
  15117. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  15118. }
  15119. if (this.navigationDivs !== undefined) {
  15120. if (this.navigationDivs['wrapper'] !== undefined) {
  15121. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  15122. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  15123. }
  15124. }
  15125. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  15126. };
  15127. /**
  15128. * Set a data set with nodes for the graph
  15129. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  15130. * @private
  15131. */
  15132. Graph.prototype._setNodes = function(nodes) {
  15133. var oldNodesData = this.nodesData;
  15134. if (nodes instanceof DataSet || nodes instanceof DataView) {
  15135. this.nodesData = nodes;
  15136. }
  15137. else if (nodes instanceof Array) {
  15138. this.nodesData = new DataSet();
  15139. this.nodesData.add(nodes);
  15140. }
  15141. else if (!nodes) {
  15142. this.nodesData = new DataSet();
  15143. }
  15144. else {
  15145. throw new TypeError('Array or DataSet expected');
  15146. }
  15147. if (oldNodesData) {
  15148. // unsubscribe from old dataset
  15149. util.forEach(this.nodesListeners, function (callback, event) {
  15150. oldNodesData.off(event, callback);
  15151. });
  15152. }
  15153. // remove drawn nodes
  15154. this.nodes = {};
  15155. if (this.nodesData) {
  15156. // subscribe to new dataset
  15157. var me = this;
  15158. util.forEach(this.nodesListeners, function (callback, event) {
  15159. me.nodesData.on(event, callback);
  15160. });
  15161. // draw all new nodes
  15162. var ids = this.nodesData.getIds();
  15163. this._addNodes(ids);
  15164. }
  15165. this._updateSelection();
  15166. };
  15167. /**
  15168. * Add nodes
  15169. * @param {Number[] | String[]} ids
  15170. * @private
  15171. */
  15172. Graph.prototype._addNodes = function(ids) {
  15173. var id;
  15174. for (var i = 0, len = ids.length; i < len; i++) {
  15175. id = ids[i];
  15176. var data = this.nodesData.get(id);
  15177. var node = new Node(data, this.images, this.groups, this.constants);
  15178. this.nodes[id] = node; // note: this may replace an existing node
  15179. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  15180. var radius = 10 * 0.1*ids.length;
  15181. var angle = 2 * Math.PI * Math.random();
  15182. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  15183. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  15184. }
  15185. this.moving = true;
  15186. }
  15187. this._updateNodeIndexList();
  15188. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15189. this._resetLevels();
  15190. this._setupHierarchicalLayout();
  15191. }
  15192. this._updateCalculationNodes();
  15193. this._reconnectEdges();
  15194. this._updateValueRange(this.nodes);
  15195. this.updateLabels();
  15196. };
  15197. /**
  15198. * Update existing nodes, or create them when not yet existing
  15199. * @param {Number[] | String[]} ids
  15200. * @private
  15201. */
  15202. Graph.prototype._updateNodes = function(ids) {
  15203. var nodes = this.nodes,
  15204. nodesData = this.nodesData;
  15205. for (var i = 0, len = ids.length; i < len; i++) {
  15206. var id = ids[i];
  15207. var node = nodes[id];
  15208. var data = nodesData.get(id);
  15209. if (node) {
  15210. // update node
  15211. node.setProperties(data, this.constants);
  15212. }
  15213. else {
  15214. // create node
  15215. node = new Node(properties, this.images, this.groups, this.constants);
  15216. nodes[id] = node;
  15217. if (!node.isFixed()) {
  15218. this.moving = true;
  15219. }
  15220. }
  15221. }
  15222. this._updateNodeIndexList();
  15223. this._reconnectEdges();
  15224. this._updateValueRange(nodes);
  15225. };
  15226. /**
  15227. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  15228. * @param {Number[] | String[]} ids
  15229. * @private
  15230. */
  15231. Graph.prototype._removeNodes = function(ids) {
  15232. var nodes = this.nodes;
  15233. for (var i = 0, len = ids.length; i < len; i++) {
  15234. var id = ids[i];
  15235. delete nodes[id];
  15236. }
  15237. this._updateNodeIndexList();
  15238. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15239. this._resetLevels();
  15240. this._setupHierarchicalLayout();
  15241. }
  15242. this._updateCalculationNodes();
  15243. this._reconnectEdges();
  15244. this._updateSelection();
  15245. this._updateValueRange(nodes);
  15246. };
  15247. /**
  15248. * Load edges by reading the data table
  15249. * @param {Array | DataSet | DataView} edges The data containing the edges.
  15250. * @private
  15251. * @private
  15252. */
  15253. Graph.prototype._setEdges = function(edges) {
  15254. var oldEdgesData = this.edgesData;
  15255. if (edges instanceof DataSet || edges instanceof DataView) {
  15256. this.edgesData = edges;
  15257. }
  15258. else if (edges instanceof Array) {
  15259. this.edgesData = new DataSet();
  15260. this.edgesData.add(edges);
  15261. }
  15262. else if (!edges) {
  15263. this.edgesData = new DataSet();
  15264. }
  15265. else {
  15266. throw new TypeError('Array or DataSet expected');
  15267. }
  15268. if (oldEdgesData) {
  15269. // unsubscribe from old dataset
  15270. util.forEach(this.edgesListeners, function (callback, event) {
  15271. oldEdgesData.off(event, callback);
  15272. });
  15273. }
  15274. // remove drawn edges
  15275. this.edges = {};
  15276. if (this.edgesData) {
  15277. // subscribe to new dataset
  15278. var me = this;
  15279. util.forEach(this.edgesListeners, function (callback, event) {
  15280. me.edgesData.on(event, callback);
  15281. });
  15282. // draw all new nodes
  15283. var ids = this.edgesData.getIds();
  15284. this._addEdges(ids);
  15285. }
  15286. this._reconnectEdges();
  15287. };
  15288. /**
  15289. * Add edges
  15290. * @param {Number[] | String[]} ids
  15291. * @private
  15292. */
  15293. Graph.prototype._addEdges = function (ids) {
  15294. var edges = this.edges,
  15295. edgesData = this.edgesData;
  15296. for (var i = 0, len = ids.length; i < len; i++) {
  15297. var id = ids[i];
  15298. var oldEdge = edges[id];
  15299. if (oldEdge) {
  15300. oldEdge.disconnect();
  15301. }
  15302. var data = edgesData.get(id, {"showInternalIds" : true});
  15303. edges[id] = new Edge(data, this, this.constants);
  15304. }
  15305. this.moving = true;
  15306. this._updateValueRange(edges);
  15307. this._createBezierNodes();
  15308. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15309. this._resetLevels();
  15310. this._setupHierarchicalLayout();
  15311. }
  15312. this._updateCalculationNodes();
  15313. };
  15314. /**
  15315. * Update existing edges, or create them when not yet existing
  15316. * @param {Number[] | String[]} ids
  15317. * @private
  15318. */
  15319. Graph.prototype._updateEdges = function (ids) {
  15320. var edges = this.edges,
  15321. edgesData = this.edgesData;
  15322. for (var i = 0, len = ids.length; i < len; i++) {
  15323. var id = ids[i];
  15324. var data = edgesData.get(id);
  15325. var edge = edges[id];
  15326. if (edge) {
  15327. // update edge
  15328. edge.disconnect();
  15329. edge.setProperties(data, this.constants);
  15330. edge.connect();
  15331. }
  15332. else {
  15333. // create edge
  15334. edge = new Edge(data, this, this.constants);
  15335. this.edges[id] = edge;
  15336. }
  15337. }
  15338. this._createBezierNodes();
  15339. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15340. this._resetLevels();
  15341. this._setupHierarchicalLayout();
  15342. }
  15343. this.moving = true;
  15344. this._updateValueRange(edges);
  15345. };
  15346. /**
  15347. * Remove existing edges. Non existing ids will be ignored
  15348. * @param {Number[] | String[]} ids
  15349. * @private
  15350. */
  15351. Graph.prototype._removeEdges = function (ids) {
  15352. var edges = this.edges;
  15353. for (var i = 0, len = ids.length; i < len; i++) {
  15354. var id = ids[i];
  15355. var edge = edges[id];
  15356. if (edge) {
  15357. if (edge.via != null) {
  15358. delete this.sectors['support']['nodes'][edge.via.id];
  15359. }
  15360. edge.disconnect();
  15361. delete edges[id];
  15362. }
  15363. }
  15364. this.moving = true;
  15365. this._updateValueRange(edges);
  15366. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15367. this._resetLevels();
  15368. this._setupHierarchicalLayout();
  15369. }
  15370. this._updateCalculationNodes();
  15371. };
  15372. /**
  15373. * Reconnect all edges
  15374. * @private
  15375. */
  15376. Graph.prototype._reconnectEdges = function() {
  15377. var id,
  15378. nodes = this.nodes,
  15379. edges = this.edges;
  15380. for (id in nodes) {
  15381. if (nodes.hasOwnProperty(id)) {
  15382. nodes[id].edges = [];
  15383. }
  15384. }
  15385. for (id in edges) {
  15386. if (edges.hasOwnProperty(id)) {
  15387. var edge = edges[id];
  15388. edge.from = null;
  15389. edge.to = null;
  15390. edge.connect();
  15391. }
  15392. }
  15393. };
  15394. /**
  15395. * Update the values of all object in the given array according to the current
  15396. * value range of the objects in the array.
  15397. * @param {Object} obj An object containing a set of Edges or Nodes
  15398. * The objects must have a method getValue() and
  15399. * setValueRange(min, max).
  15400. * @private
  15401. */
  15402. Graph.prototype._updateValueRange = function(obj) {
  15403. var id;
  15404. // determine the range of the objects
  15405. var valueMin = undefined;
  15406. var valueMax = undefined;
  15407. for (id in obj) {
  15408. if (obj.hasOwnProperty(id)) {
  15409. var value = obj[id].getValue();
  15410. if (value !== undefined) {
  15411. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  15412. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  15413. }
  15414. }
  15415. }
  15416. // adjust the range of all objects
  15417. if (valueMin !== undefined && valueMax !== undefined) {
  15418. for (id in obj) {
  15419. if (obj.hasOwnProperty(id)) {
  15420. obj[id].setValueRange(valueMin, valueMax);
  15421. }
  15422. }
  15423. }
  15424. };
  15425. /**
  15426. * Redraw the graph with the current data
  15427. * chart will be resized too.
  15428. */
  15429. Graph.prototype.redraw = function() {
  15430. this.setSize(this.width, this.height);
  15431. this._redraw();
  15432. };
  15433. /**
  15434. * Redraw the graph with the current data
  15435. * @private
  15436. */
  15437. Graph.prototype._redraw = function() {
  15438. var ctx = this.frame.canvas.getContext('2d');
  15439. // clear the canvas
  15440. var w = this.frame.canvas.width;
  15441. var h = this.frame.canvas.height;
  15442. ctx.clearRect(0, 0, w, h);
  15443. // set scaling and translation
  15444. ctx.save();
  15445. ctx.translate(this.translation.x, this.translation.y);
  15446. ctx.scale(this.scale, this.scale);
  15447. this.canvasTopLeft = {
  15448. "x": this._canvasToX(0),
  15449. "y": this._canvasToY(0)
  15450. };
  15451. this.canvasBottomRight = {
  15452. "x": this._canvasToX(this.frame.canvas.clientWidth),
  15453. "y": this._canvasToY(this.frame.canvas.clientHeight)
  15454. };
  15455. this._doInAllSectors("_drawAllSectorNodes",ctx);
  15456. this._doInAllSectors("_drawEdges",ctx);
  15457. this._doInAllSectors("_drawNodes",ctx,false);
  15458. // this._doInSupportSector("_drawNodes",ctx,true);
  15459. // this._drawTree(ctx,"#F00F0F");
  15460. // restore original scaling and translation
  15461. ctx.restore();
  15462. };
  15463. /**
  15464. * Set the translation of the graph
  15465. * @param {Number} offsetX Horizontal offset
  15466. * @param {Number} offsetY Vertical offset
  15467. * @private
  15468. */
  15469. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  15470. if (this.translation === undefined) {
  15471. this.translation = {
  15472. x: 0,
  15473. y: 0
  15474. };
  15475. }
  15476. if (offsetX !== undefined) {
  15477. this.translation.x = offsetX;
  15478. }
  15479. if (offsetY !== undefined) {
  15480. this.translation.y = offsetY;
  15481. }
  15482. };
  15483. /**
  15484. * Get the translation of the graph
  15485. * @return {Object} translation An object with parameters x and y, both a number
  15486. * @private
  15487. */
  15488. Graph.prototype._getTranslation = function() {
  15489. return {
  15490. x: this.translation.x,
  15491. y: this.translation.y
  15492. };
  15493. };
  15494. /**
  15495. * Scale the graph
  15496. * @param {Number} scale Scaling factor 1.0 is unscaled
  15497. * @private
  15498. */
  15499. Graph.prototype._setScale = function(scale) {
  15500. this.scale = scale;
  15501. };
  15502. /**
  15503. * Get the current scale of the graph
  15504. * @return {Number} scale Scaling factor 1.0 is unscaled
  15505. * @private
  15506. */
  15507. Graph.prototype._getScale = function() {
  15508. return this.scale;
  15509. };
  15510. /**
  15511. * Convert a horizontal point on the HTML canvas to the x-value of the model
  15512. * @param {number} x
  15513. * @returns {number}
  15514. * @private
  15515. */
  15516. Graph.prototype._canvasToX = function(x) {
  15517. return (x - this.translation.x) / this.scale;
  15518. };
  15519. /**
  15520. * Convert an x-value in the model to a horizontal point on the HTML canvas
  15521. * @param {number} x
  15522. * @returns {number}
  15523. * @private
  15524. */
  15525. Graph.prototype._xToCanvas = function(x) {
  15526. return x * this.scale + this.translation.x;
  15527. };
  15528. /**
  15529. * Convert a vertical point on the HTML canvas to the y-value of the model
  15530. * @param {number} y
  15531. * @returns {number}
  15532. * @private
  15533. */
  15534. Graph.prototype._canvasToY = function(y) {
  15535. return (y - this.translation.y) / this.scale;
  15536. };
  15537. /**
  15538. * Convert an y-value in the model to a vertical point on the HTML canvas
  15539. * @param {number} y
  15540. * @returns {number}
  15541. * @private
  15542. */
  15543. Graph.prototype._yToCanvas = function(y) {
  15544. return y * this.scale + this.translation.y ;
  15545. };
  15546. /**
  15547. * Redraw all nodes
  15548. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15549. * @param {CanvasRenderingContext2D} ctx
  15550. * @param {Boolean} [alwaysShow]
  15551. * @private
  15552. */
  15553. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  15554. if (alwaysShow === undefined) {
  15555. alwaysShow = false;
  15556. }
  15557. // first draw the unselected nodes
  15558. var nodes = this.nodes;
  15559. var selected = [];
  15560. for (var id in nodes) {
  15561. if (nodes.hasOwnProperty(id)) {
  15562. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  15563. if (nodes[id].isSelected()) {
  15564. selected.push(id);
  15565. }
  15566. else {
  15567. if (nodes[id].inArea() || alwaysShow) {
  15568. nodes[id].draw(ctx);
  15569. }
  15570. }
  15571. }
  15572. }
  15573. // draw the selected nodes on top
  15574. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  15575. if (nodes[selected[s]].inArea() || alwaysShow) {
  15576. nodes[selected[s]].draw(ctx);
  15577. }
  15578. }
  15579. };
  15580. /**
  15581. * Redraw all edges
  15582. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15583. * @param {CanvasRenderingContext2D} ctx
  15584. * @private
  15585. */
  15586. Graph.prototype._drawEdges = function(ctx) {
  15587. var edges = this.edges;
  15588. for (var id in edges) {
  15589. if (edges.hasOwnProperty(id)) {
  15590. var edge = edges[id];
  15591. edge.setScale(this.scale);
  15592. if (edge.connected) {
  15593. edges[id].draw(ctx);
  15594. }
  15595. }
  15596. }
  15597. };
  15598. /**
  15599. * Find a stable position for all nodes
  15600. * @private
  15601. */
  15602. Graph.prototype._stabilize = function() {
  15603. if (this.constants.freezeForStabilization == true) {
  15604. this._freezeDefinedNodes();
  15605. }
  15606. // find stable position
  15607. var count = 0;
  15608. while (this.moving && count < this.constants.stabilizationIterations) {
  15609. this._physicsTick();
  15610. count++;
  15611. }
  15612. this.zoomExtent(false,true);
  15613. if (this.constants.freezeForStabilization == true) {
  15614. this._restoreFrozenNodes();
  15615. }
  15616. this.emit("stabilized",{iterations:count});
  15617. };
  15618. Graph.prototype._freezeDefinedNodes = function() {
  15619. var nodes = this.nodes;
  15620. for (var id in nodes) {
  15621. if (nodes.hasOwnProperty(id)) {
  15622. if (nodes[id].x != null && nodes[id].y != null) {
  15623. nodes[id].fixedData.x = nodes[id].xFixed;
  15624. nodes[id].fixedData.y = nodes[id].yFixed;
  15625. nodes[id].xFixed = true;
  15626. nodes[id].yFixed = true;
  15627. }
  15628. }
  15629. }
  15630. };
  15631. Graph.prototype._restoreFrozenNodes = function() {
  15632. var nodes = this.nodes;
  15633. for (var id in nodes) {
  15634. if (nodes.hasOwnProperty(id)) {
  15635. if (nodes[id].fixedData.x != null) {
  15636. nodes[id].xFixed = nodes[id].fixedData.x;
  15637. nodes[id].yFixed = nodes[id].fixedData.y;
  15638. }
  15639. }
  15640. }
  15641. };
  15642. /**
  15643. * Check if any of the nodes is still moving
  15644. * @param {number} vmin the minimum velocity considered as 'moving'
  15645. * @return {boolean} true if moving, false if non of the nodes is moving
  15646. * @private
  15647. */
  15648. Graph.prototype._isMoving = function(vmin) {
  15649. var nodes = this.nodes;
  15650. for (var id in nodes) {
  15651. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15652. return true;
  15653. }
  15654. }
  15655. return false;
  15656. };
  15657. /**
  15658. * /**
  15659. * Perform one discrete step for all nodes
  15660. *
  15661. * @private
  15662. */
  15663. Graph.prototype._discreteStepNodes = function() {
  15664. var interval = this.physicsDiscreteStepsize;
  15665. var nodes = this.nodes;
  15666. var nodeId;
  15667. var nodesPresent = false;
  15668. if (this.constants.maxVelocity > 0) {
  15669. for (nodeId in nodes) {
  15670. if (nodes.hasOwnProperty(nodeId)) {
  15671. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  15672. nodesPresent = true;
  15673. }
  15674. }
  15675. }
  15676. else {
  15677. for (nodeId in nodes) {
  15678. if (nodes.hasOwnProperty(nodeId)) {
  15679. nodes[nodeId].discreteStep(interval);
  15680. nodesPresent = true;
  15681. }
  15682. }
  15683. }
  15684. if (nodesPresent == true) {
  15685. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  15686. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  15687. this.moving = true;
  15688. }
  15689. else {
  15690. this.moving = this._isMoving(vminCorrected);
  15691. }
  15692. }
  15693. };
  15694. Graph.prototype._physicsTick = function() {
  15695. if (!this.freezeSimulation) {
  15696. if (this.moving) {
  15697. this._doInAllActiveSectors("_initializeForceCalculation");
  15698. this._doInAllActiveSectors("_discreteStepNodes");
  15699. if (this.constants.smoothCurves) {
  15700. this._doInSupportSector("_discreteStepNodes");
  15701. }
  15702. this._findCenter(this._getRange())
  15703. }
  15704. }
  15705. };
  15706. /**
  15707. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  15708. * It reschedules itself at the beginning of the function
  15709. *
  15710. * @private
  15711. */
  15712. Graph.prototype._animationStep = function() {
  15713. // reset the timer so a new scheduled animation step can be set
  15714. this.timer = undefined;
  15715. // handle the keyboad movement
  15716. this._handleNavigation();
  15717. // this schedules a new animation step
  15718. this.start();
  15719. // start the physics simulation
  15720. var calculationTime = Date.now();
  15721. var maxSteps = 1;
  15722. this._physicsTick();
  15723. var timeRequired = Date.now() - calculationTime;
  15724. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  15725. this._physicsTick();
  15726. timeRequired = Date.now() - calculationTime;
  15727. maxSteps++;
  15728. }
  15729. // start the rendering process
  15730. var renderTime = Date.now();
  15731. this._redraw();
  15732. this.renderTime = Date.now() - renderTime;
  15733. };
  15734. if (typeof window !== 'undefined') {
  15735. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  15736. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  15737. }
  15738. /**
  15739. * Schedule a animation step with the refreshrate interval.
  15740. */
  15741. Graph.prototype.start = function() {
  15742. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  15743. if (!this.timer) {
  15744. var ua = navigator.userAgent.toLowerCase();
  15745. var requiresTimeout = false;
  15746. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  15747. requiresTimeout = true;
  15748. }
  15749. else if (ua.indexOf('safari') != -1) { // safari
  15750. if (ua.indexOf('chrome') <= -1) {
  15751. requiresTimeout = true;
  15752. }
  15753. }
  15754. if (requiresTimeout == true) {
  15755. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15756. }
  15757. else{
  15758. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15759. }
  15760. }
  15761. }
  15762. else {
  15763. this._redraw();
  15764. }
  15765. };
  15766. /**
  15767. * Move the graph according to the keyboard presses.
  15768. *
  15769. * @private
  15770. */
  15771. Graph.prototype._handleNavigation = function() {
  15772. if (this.xIncrement != 0 || this.yIncrement != 0) {
  15773. var translation = this._getTranslation();
  15774. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  15775. }
  15776. if (this.zoomIncrement != 0) {
  15777. var center = {
  15778. x: this.frame.canvas.clientWidth / 2,
  15779. y: this.frame.canvas.clientHeight / 2
  15780. };
  15781. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  15782. }
  15783. };
  15784. /**
  15785. * Freeze the _animationStep
  15786. */
  15787. Graph.prototype.toggleFreeze = function() {
  15788. if (this.freezeSimulation == false) {
  15789. this.freezeSimulation = true;
  15790. }
  15791. else {
  15792. this.freezeSimulation = false;
  15793. this.start();
  15794. }
  15795. };
  15796. Graph.prototype._configureSmoothCurves = function(disableStart) {
  15797. if (disableStart === undefined) {
  15798. disableStart = true;
  15799. }
  15800. if (this.constants.smoothCurves == true) {
  15801. this._createBezierNodes();
  15802. }
  15803. else {
  15804. // delete the support nodes
  15805. this.sectors['support']['nodes'] = {};
  15806. for (var edgeId in this.edges) {
  15807. if (this.edges.hasOwnProperty(edgeId)) {
  15808. this.edges[edgeId].smooth = false;
  15809. this.edges[edgeId].via = null;
  15810. }
  15811. }
  15812. }
  15813. this._updateCalculationNodes();
  15814. if (!disableStart) {
  15815. this.moving = true;
  15816. this.start();
  15817. }
  15818. };
  15819. Graph.prototype._createBezierNodes = function() {
  15820. if (this.constants.smoothCurves == true) {
  15821. for (var edgeId in this.edges) {
  15822. if (this.edges.hasOwnProperty(edgeId)) {
  15823. var edge = this.edges[edgeId];
  15824. if (edge.via == null) {
  15825. edge.smooth = true;
  15826. var nodeId = "edgeId:".concat(edge.id);
  15827. this.sectors['support']['nodes'][nodeId] = new Node(
  15828. {id:nodeId,
  15829. mass:1,
  15830. shape:'circle',
  15831. image:"",
  15832. internalMultiplier:1
  15833. },{},{},this.constants);
  15834. edge.via = this.sectors['support']['nodes'][nodeId];
  15835. edge.via.parentEdgeId = edge.id;
  15836. edge.positionBezierNode();
  15837. }
  15838. }
  15839. }
  15840. }
  15841. };
  15842. Graph.prototype._initializeMixinLoaders = function () {
  15843. for (var mixinFunction in graphMixinLoaders) {
  15844. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  15845. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  15846. }
  15847. }
  15848. };
  15849. /**
  15850. * Load the XY positions of the nodes into the dataset.
  15851. */
  15852. Graph.prototype.storePosition = function() {
  15853. var dataArray = [];
  15854. for (var nodeId in this.nodes) {
  15855. if (this.nodes.hasOwnProperty(nodeId)) {
  15856. var node = this.nodes[nodeId];
  15857. var allowedToMoveX = !this.nodes.xFixed;
  15858. var allowedToMoveY = !this.nodes.yFixed;
  15859. if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
  15860. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  15861. }
  15862. }
  15863. }
  15864. this.nodesData.update(dataArray);
  15865. };
  15866. /**
  15867. * vis.js module exports
  15868. */
  15869. var vis = {
  15870. util: util,
  15871. Controller: Controller,
  15872. DataSet: DataSet,
  15873. DataView: DataView,
  15874. Range: Range,
  15875. Stack: Stack,
  15876. TimeStep: TimeStep,
  15877. components: {
  15878. items: {
  15879. Item: Item,
  15880. ItemBox: ItemBox,
  15881. ItemPoint: ItemPoint,
  15882. ItemRange: ItemRange
  15883. },
  15884. Component: Component,
  15885. Panel: Panel,
  15886. RootPanel: RootPanel,
  15887. ItemSet: ItemSet,
  15888. TimeAxis: TimeAxis
  15889. },
  15890. graph: {
  15891. Node: Node,
  15892. Edge: Edge,
  15893. Popup: Popup,
  15894. Groups: Groups,
  15895. Images: Images
  15896. },
  15897. Timeline: Timeline,
  15898. Graph: Graph
  15899. };
  15900. /**
  15901. * CommonJS module exports
  15902. */
  15903. if (typeof exports !== 'undefined') {
  15904. exports = vis;
  15905. }
  15906. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  15907. module.exports = vis;
  15908. }
  15909. /**
  15910. * AMD module exports
  15911. */
  15912. if (typeof(define) === 'function') {
  15913. define(function () {
  15914. return vis;
  15915. });
  15916. }
  15917. /**
  15918. * Window exports
  15919. */
  15920. if (typeof window !== 'undefined') {
  15921. // attach the module to the window, load as a regular javascript file
  15922. window['vis'] = vis;
  15923. }
  15924. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  15925. /**
  15926. * Expose `Emitter`.
  15927. */
  15928. module.exports = Emitter;
  15929. /**
  15930. * Initialize a new `Emitter`.
  15931. *
  15932. * @api public
  15933. */
  15934. function Emitter(obj) {
  15935. if (obj) return mixin(obj);
  15936. };
  15937. /**
  15938. * Mixin the emitter properties.
  15939. *
  15940. * @param {Object} obj
  15941. * @return {Object}
  15942. * @api private
  15943. */
  15944. function mixin(obj) {
  15945. for (var key in Emitter.prototype) {
  15946. obj[key] = Emitter.prototype[key];
  15947. }
  15948. return obj;
  15949. }
  15950. /**
  15951. * Listen on the given `event` with `fn`.
  15952. *
  15953. * @param {String} event
  15954. * @param {Function} fn
  15955. * @return {Emitter}
  15956. * @api public
  15957. */
  15958. Emitter.prototype.on =
  15959. Emitter.prototype.addEventListener = function(event, fn){
  15960. this._callbacks = this._callbacks || {};
  15961. (this._callbacks[event] = this._callbacks[event] || [])
  15962. .push(fn);
  15963. return this;
  15964. };
  15965. /**
  15966. * Adds an `event` listener that will be invoked a single
  15967. * time then automatically removed.
  15968. *
  15969. * @param {String} event
  15970. * @param {Function} fn
  15971. * @return {Emitter}
  15972. * @api public
  15973. */
  15974. Emitter.prototype.once = function(event, fn){
  15975. var self = this;
  15976. this._callbacks = this._callbacks || {};
  15977. function on() {
  15978. self.off(event, on);
  15979. fn.apply(this, arguments);
  15980. }
  15981. on.fn = fn;
  15982. this.on(event, on);
  15983. return this;
  15984. };
  15985. /**
  15986. * Remove the given callback for `event` or all
  15987. * registered callbacks.
  15988. *
  15989. * @param {String} event
  15990. * @param {Function} fn
  15991. * @return {Emitter}
  15992. * @api public
  15993. */
  15994. Emitter.prototype.off =
  15995. Emitter.prototype.removeListener =
  15996. Emitter.prototype.removeAllListeners =
  15997. Emitter.prototype.removeEventListener = function(event, fn){
  15998. this._callbacks = this._callbacks || {};
  15999. // all
  16000. if (0 == arguments.length) {
  16001. this._callbacks = {};
  16002. return this;
  16003. }
  16004. // specific event
  16005. var callbacks = this._callbacks[event];
  16006. if (!callbacks) return this;
  16007. // remove all handlers
  16008. if (1 == arguments.length) {
  16009. delete this._callbacks[event];
  16010. return this;
  16011. }
  16012. // remove specific handler
  16013. var cb;
  16014. for (var i = 0; i < callbacks.length; i++) {
  16015. cb = callbacks[i];
  16016. if (cb === fn || cb.fn === fn) {
  16017. callbacks.splice(i, 1);
  16018. break;
  16019. }
  16020. }
  16021. return this;
  16022. };
  16023. /**
  16024. * Emit `event` with the given args.
  16025. *
  16026. * @param {String} event
  16027. * @param {Mixed} ...
  16028. * @return {Emitter}
  16029. */
  16030. Emitter.prototype.emit = function(event){
  16031. this._callbacks = this._callbacks || {};
  16032. var args = [].slice.call(arguments, 1)
  16033. , callbacks = this._callbacks[event];
  16034. if (callbacks) {
  16035. callbacks = callbacks.slice(0);
  16036. for (var i = 0, len = callbacks.length; i < len; ++i) {
  16037. callbacks[i].apply(this, args);
  16038. }
  16039. }
  16040. return this;
  16041. };
  16042. /**
  16043. * Return array of callbacks for `event`.
  16044. *
  16045. * @param {String} event
  16046. * @return {Array}
  16047. * @api public
  16048. */
  16049. Emitter.prototype.listeners = function(event){
  16050. this._callbacks = this._callbacks || {};
  16051. return this._callbacks[event] || [];
  16052. };
  16053. /**
  16054. * Check if this emitter has `event` handlers.
  16055. *
  16056. * @param {String} event
  16057. * @return {Boolean}
  16058. * @api public
  16059. */
  16060. Emitter.prototype.hasListeners = function(event){
  16061. return !! this.listeners(event).length;
  16062. };
  16063. },{}],3:[function(require,module,exports){
  16064. /*! Hammer.JS - v1.0.5 - 2013-04-07
  16065. * http://eightmedia.github.com/hammer.js
  16066. *
  16067. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  16068. * Licensed under the MIT license */
  16069. (function(window, undefined) {
  16070. 'use strict';
  16071. /**
  16072. * Hammer
  16073. * use this to create instances
  16074. * @param {HTMLElement} element
  16075. * @param {Object} options
  16076. * @returns {Hammer.Instance}
  16077. * @constructor
  16078. */
  16079. var Hammer = function(element, options) {
  16080. return new Hammer.Instance(element, options || {});
  16081. };
  16082. // default settings
  16083. Hammer.defaults = {
  16084. // add styles and attributes to the element to prevent the browser from doing
  16085. // its native behavior. this doesnt prevent the scrolling, but cancels
  16086. // the contextmenu, tap highlighting etc
  16087. // set to false to disable this
  16088. stop_browser_behavior: {
  16089. // this also triggers onselectstart=false for IE
  16090. userSelect: 'none',
  16091. // this makes the element blocking in IE10 >, you could experiment with the value
  16092. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  16093. touchAction: 'none',
  16094. touchCallout: 'none',
  16095. contentZooming: 'none',
  16096. userDrag: 'none',
  16097. tapHighlightColor: 'rgba(0,0,0,0)'
  16098. }
  16099. // more settings are defined per gesture at gestures.js
  16100. };
  16101. // detect touchevents
  16102. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  16103. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  16104. // dont use mouseevents on mobile devices
  16105. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  16106. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  16107. // eventtypes per touchevent (start, move, end)
  16108. // are filled by Hammer.event.determineEventTypes on setup
  16109. Hammer.EVENT_TYPES = {};
  16110. // direction defines
  16111. Hammer.DIRECTION_DOWN = 'down';
  16112. Hammer.DIRECTION_LEFT = 'left';
  16113. Hammer.DIRECTION_UP = 'up';
  16114. Hammer.DIRECTION_RIGHT = 'right';
  16115. // pointer type
  16116. Hammer.POINTER_MOUSE = 'mouse';
  16117. Hammer.POINTER_TOUCH = 'touch';
  16118. Hammer.POINTER_PEN = 'pen';
  16119. // touch event defines
  16120. Hammer.EVENT_START = 'start';
  16121. Hammer.EVENT_MOVE = 'move';
  16122. Hammer.EVENT_END = 'end';
  16123. // hammer document where the base events are added at
  16124. Hammer.DOCUMENT = document;
  16125. // plugins namespace
  16126. Hammer.plugins = {};
  16127. // if the window events are set...
  16128. Hammer.READY = false;
  16129. /**
  16130. * setup events to detect gestures on the document
  16131. */
  16132. function setup() {
  16133. if(Hammer.READY) {
  16134. return;
  16135. }
  16136. // find what eventtypes we add listeners to
  16137. Hammer.event.determineEventTypes();
  16138. // Register all gestures inside Hammer.gestures
  16139. for(var name in Hammer.gestures) {
  16140. if(Hammer.gestures.hasOwnProperty(name)) {
  16141. Hammer.detection.register(Hammer.gestures[name]);
  16142. }
  16143. }
  16144. // Add touch events on the document
  16145. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  16146. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  16147. // Hammer is ready...!
  16148. Hammer.READY = true;
  16149. }
  16150. /**
  16151. * create new hammer instance
  16152. * all methods should return the instance itself, so it is chainable.
  16153. * @param {HTMLElement} element
  16154. * @param {Object} [options={}]
  16155. * @returns {Hammer.Instance}
  16156. * @constructor
  16157. */
  16158. Hammer.Instance = function(element, options) {
  16159. var self = this;
  16160. // setup HammerJS window events and register all gestures
  16161. // this also sets up the default options
  16162. setup();
  16163. this.element = element;
  16164. // start/stop detection option
  16165. this.enabled = true;
  16166. // merge options
  16167. this.options = Hammer.utils.extend(
  16168. Hammer.utils.extend({}, Hammer.defaults),
  16169. options || {});
  16170. // add some css to the element to prevent the browser from doing its native behavoir
  16171. if(this.options.stop_browser_behavior) {
  16172. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  16173. }
  16174. // start detection on touchstart
  16175. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  16176. if(self.enabled) {
  16177. Hammer.detection.startDetect(self, ev);
  16178. }
  16179. });
  16180. // return instance
  16181. return this;
  16182. };
  16183. Hammer.Instance.prototype = {
  16184. /**
  16185. * bind events to the instance
  16186. * @param {String} gesture
  16187. * @param {Function} handler
  16188. * @returns {Hammer.Instance}
  16189. */
  16190. on: function onEvent(gesture, handler){
  16191. var gestures = gesture.split(' ');
  16192. for(var t=0; t<gestures.length; t++) {
  16193. this.element.addEventListener(gestures[t], handler, false);
  16194. }
  16195. return this;
  16196. },
  16197. /**
  16198. * unbind events to the instance
  16199. * @param {String} gesture
  16200. * @param {Function} handler
  16201. * @returns {Hammer.Instance}
  16202. */
  16203. off: function offEvent(gesture, handler){
  16204. var gestures = gesture.split(' ');
  16205. for(var t=0; t<gestures.length; t++) {
  16206. this.element.removeEventListener(gestures[t], handler, false);
  16207. }
  16208. return this;
  16209. },
  16210. /**
  16211. * trigger gesture event
  16212. * @param {String} gesture
  16213. * @param {Object} eventData
  16214. * @returns {Hammer.Instance}
  16215. */
  16216. trigger: function triggerEvent(gesture, eventData){
  16217. // create DOM event
  16218. var event = Hammer.DOCUMENT.createEvent('Event');
  16219. event.initEvent(gesture, true, true);
  16220. event.gesture = eventData;
  16221. // trigger on the target if it is in the instance element,
  16222. // this is for event delegation tricks
  16223. var element = this.element;
  16224. if(Hammer.utils.hasParent(eventData.target, element)) {
  16225. element = eventData.target;
  16226. }
  16227. element.dispatchEvent(event);
  16228. return this;
  16229. },
  16230. /**
  16231. * enable of disable hammer.js detection
  16232. * @param {Boolean} state
  16233. * @returns {Hammer.Instance}
  16234. */
  16235. enable: function enable(state) {
  16236. this.enabled = state;
  16237. return this;
  16238. }
  16239. };
  16240. /**
  16241. * this holds the last move event,
  16242. * used to fix empty touchend issue
  16243. * see the onTouch event for an explanation
  16244. * @type {Object}
  16245. */
  16246. var last_move_event = null;
  16247. /**
  16248. * when the mouse is hold down, this is true
  16249. * @type {Boolean}
  16250. */
  16251. var enable_detect = false;
  16252. /**
  16253. * when touch events have been fired, this is true
  16254. * @type {Boolean}
  16255. */
  16256. var touch_triggered = false;
  16257. Hammer.event = {
  16258. /**
  16259. * simple addEventListener
  16260. * @param {HTMLElement} element
  16261. * @param {String} type
  16262. * @param {Function} handler
  16263. */
  16264. bindDom: function(element, type, handler) {
  16265. var types = type.split(' ');
  16266. for(var t=0; t<types.length; t++) {
  16267. element.addEventListener(types[t], handler, false);
  16268. }
  16269. },
  16270. /**
  16271. * touch events with mouse fallback
  16272. * @param {HTMLElement} element
  16273. * @param {String} eventType like Hammer.EVENT_MOVE
  16274. * @param {Function} handler
  16275. */
  16276. onTouch: function onTouch(element, eventType, handler) {
  16277. var self = this;
  16278. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  16279. var sourceEventType = ev.type.toLowerCase();
  16280. // onmouseup, but when touchend has been fired we do nothing.
  16281. // this is for touchdevices which also fire a mouseup on touchend
  16282. if(sourceEventType.match(/mouse/) && touch_triggered) {
  16283. return;
  16284. }
  16285. // mousebutton must be down or a touch event
  16286. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  16287. sourceEventType.match(/pointerdown/) || // pointerevents touch
  16288. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  16289. ){
  16290. enable_detect = true;
  16291. }
  16292. // we are in a touch event, set the touch triggered bool to true,
  16293. // this for the conflicts that may occur on ios and android
  16294. if(sourceEventType.match(/touch|pointer/)) {
  16295. touch_triggered = true;
  16296. }
  16297. // count the total touches on the screen
  16298. var count_touches = 0;
  16299. // when touch has been triggered in this detection session
  16300. // and we are now handling a mouse event, we stop that to prevent conflicts
  16301. if(enable_detect) {
  16302. // update pointerevent
  16303. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  16304. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  16305. }
  16306. // touch
  16307. else if(sourceEventType.match(/touch/)) {
  16308. count_touches = ev.touches.length;
  16309. }
  16310. // mouse
  16311. else if(!touch_triggered) {
  16312. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  16313. }
  16314. // if we are in a end event, but when we remove one touch and
  16315. // we still have enough, set eventType to move
  16316. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  16317. eventType = Hammer.EVENT_MOVE;
  16318. }
  16319. // no touches, force the end event
  16320. else if(!count_touches) {
  16321. eventType = Hammer.EVENT_END;
  16322. }
  16323. // because touchend has no touches, and we often want to use these in our gestures,
  16324. // we send the last move event as our eventData in touchend
  16325. if(!count_touches && last_move_event !== null) {
  16326. ev = last_move_event;
  16327. }
  16328. // store the last move event
  16329. else {
  16330. last_move_event = ev;
  16331. }
  16332. // trigger the handler
  16333. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  16334. // remove pointerevent from list
  16335. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  16336. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  16337. }
  16338. }
  16339. //debug(sourceEventType +" "+ eventType);
  16340. // on the end we reset everything
  16341. if(!count_touches) {
  16342. last_move_event = null;
  16343. enable_detect = false;
  16344. touch_triggered = false;
  16345. Hammer.PointerEvent.reset();
  16346. }
  16347. });
  16348. },
  16349. /**
  16350. * we have different events for each device/browser
  16351. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  16352. */
  16353. determineEventTypes: function determineEventTypes() {
  16354. // determine the eventtype we want to set
  16355. var types;
  16356. // pointerEvents magic
  16357. if(Hammer.HAS_POINTEREVENTS) {
  16358. types = Hammer.PointerEvent.getEvents();
  16359. }
  16360. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  16361. else if(Hammer.NO_MOUSEEVENTS) {
  16362. types = [
  16363. 'touchstart',
  16364. 'touchmove',
  16365. 'touchend touchcancel'];
  16366. }
  16367. // for non pointer events browsers and mixed browsers,
  16368. // like chrome on windows8 touch laptop
  16369. else {
  16370. types = [
  16371. 'touchstart mousedown',
  16372. 'touchmove mousemove',
  16373. 'touchend touchcancel mouseup'];
  16374. }
  16375. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  16376. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  16377. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  16378. },
  16379. /**
  16380. * create touchlist depending on the event
  16381. * @param {Object} ev
  16382. * @param {String} eventType used by the fakemultitouch plugin
  16383. */
  16384. getTouchList: function getTouchList(ev/*, eventType*/) {
  16385. // get the fake pointerEvent touchlist
  16386. if(Hammer.HAS_POINTEREVENTS) {
  16387. return Hammer.PointerEvent.getTouchList();
  16388. }
  16389. // get the touchlist
  16390. else if(ev.touches) {
  16391. return ev.touches;
  16392. }
  16393. // make fake touchlist from mouse position
  16394. else {
  16395. return [{
  16396. identifier: 1,
  16397. pageX: ev.pageX,
  16398. pageY: ev.pageY,
  16399. target: ev.target
  16400. }];
  16401. }
  16402. },
  16403. /**
  16404. * collect event data for Hammer js
  16405. * @param {HTMLElement} element
  16406. * @param {String} eventType like Hammer.EVENT_MOVE
  16407. * @param {Object} eventData
  16408. */
  16409. collectEventData: function collectEventData(element, eventType, ev) {
  16410. var touches = this.getTouchList(ev, eventType);
  16411. // find out pointerType
  16412. var pointerType = Hammer.POINTER_TOUCH;
  16413. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  16414. pointerType = Hammer.POINTER_MOUSE;
  16415. }
  16416. return {
  16417. center : Hammer.utils.getCenter(touches),
  16418. timeStamp : new Date().getTime(),
  16419. target : ev.target,
  16420. touches : touches,
  16421. eventType : eventType,
  16422. pointerType : pointerType,
  16423. srcEvent : ev,
  16424. /**
  16425. * prevent the browser default actions
  16426. * mostly used to disable scrolling of the browser
  16427. */
  16428. preventDefault: function() {
  16429. if(this.srcEvent.preventManipulation) {
  16430. this.srcEvent.preventManipulation();
  16431. }
  16432. if(this.srcEvent.preventDefault) {
  16433. this.srcEvent.preventDefault();
  16434. }
  16435. },
  16436. /**
  16437. * stop bubbling the event up to its parents
  16438. */
  16439. stopPropagation: function() {
  16440. this.srcEvent.stopPropagation();
  16441. },
  16442. /**
  16443. * immediately stop gesture detection
  16444. * might be useful after a swipe was detected
  16445. * @return {*}
  16446. */
  16447. stopDetect: function() {
  16448. return Hammer.detection.stopDetect();
  16449. }
  16450. };
  16451. }
  16452. };
  16453. Hammer.PointerEvent = {
  16454. /**
  16455. * holds all pointers
  16456. * @type {Object}
  16457. */
  16458. pointers: {},
  16459. /**
  16460. * get a list of pointers
  16461. * @returns {Array} touchlist
  16462. */
  16463. getTouchList: function() {
  16464. var self = this;
  16465. var touchlist = [];
  16466. // we can use forEach since pointerEvents only is in IE10
  16467. Object.keys(self.pointers).sort().forEach(function(id) {
  16468. touchlist.push(self.pointers[id]);
  16469. });
  16470. return touchlist;
  16471. },
  16472. /**
  16473. * update the position of a pointer
  16474. * @param {String} type Hammer.EVENT_END
  16475. * @param {Object} pointerEvent
  16476. */
  16477. updatePointer: function(type, pointerEvent) {
  16478. if(type == Hammer.EVENT_END) {
  16479. this.pointers = {};
  16480. }
  16481. else {
  16482. pointerEvent.identifier = pointerEvent.pointerId;
  16483. this.pointers[pointerEvent.pointerId] = pointerEvent;
  16484. }
  16485. return Object.keys(this.pointers).length;
  16486. },
  16487. /**
  16488. * check if ev matches pointertype
  16489. * @param {String} pointerType Hammer.POINTER_MOUSE
  16490. * @param {PointerEvent} ev
  16491. */
  16492. matchType: function(pointerType, ev) {
  16493. if(!ev.pointerType) {
  16494. return false;
  16495. }
  16496. var types = {};
  16497. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  16498. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  16499. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  16500. return types[pointerType];
  16501. },
  16502. /**
  16503. * get events
  16504. */
  16505. getEvents: function() {
  16506. return [
  16507. 'pointerdown MSPointerDown',
  16508. 'pointermove MSPointerMove',
  16509. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  16510. ];
  16511. },
  16512. /**
  16513. * reset the list
  16514. */
  16515. reset: function() {
  16516. this.pointers = {};
  16517. }
  16518. };
  16519. Hammer.utils = {
  16520. /**
  16521. * extend method,
  16522. * also used for cloning when dest is an empty object
  16523. * @param {Object} dest
  16524. * @param {Object} src
  16525. * @parm {Boolean} merge do a merge
  16526. * @returns {Object} dest
  16527. */
  16528. extend: function extend(dest, src, merge) {
  16529. for (var key in src) {
  16530. if(dest[key] !== undefined && merge) {
  16531. continue;
  16532. }
  16533. dest[key] = src[key];
  16534. }
  16535. return dest;
  16536. },
  16537. /**
  16538. * find if a node is in the given parent
  16539. * used for event delegation tricks
  16540. * @param {HTMLElement} node
  16541. * @param {HTMLElement} parent
  16542. * @returns {boolean} has_parent
  16543. */
  16544. hasParent: function(node, parent) {
  16545. while(node){
  16546. if(node == parent) {
  16547. return true;
  16548. }
  16549. node = node.parentNode;
  16550. }
  16551. return false;
  16552. },
  16553. /**
  16554. * get the center of all the touches
  16555. * @param {Array} touches
  16556. * @returns {Object} center
  16557. */
  16558. getCenter: function getCenter(touches) {
  16559. var valuesX = [], valuesY = [];
  16560. for(var t= 0,len=touches.length; t<len; t++) {
  16561. valuesX.push(touches[t].pageX);
  16562. valuesY.push(touches[t].pageY);
  16563. }
  16564. return {
  16565. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  16566. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  16567. };
  16568. },
  16569. /**
  16570. * calculate the velocity between two points
  16571. * @param {Number} delta_time
  16572. * @param {Number} delta_x
  16573. * @param {Number} delta_y
  16574. * @returns {Object} velocity
  16575. */
  16576. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  16577. return {
  16578. x: Math.abs(delta_x / delta_time) || 0,
  16579. y: Math.abs(delta_y / delta_time) || 0
  16580. };
  16581. },
  16582. /**
  16583. * calculate the angle between two coordinates
  16584. * @param {Touch} touch1
  16585. * @param {Touch} touch2
  16586. * @returns {Number} angle
  16587. */
  16588. getAngle: function getAngle(touch1, touch2) {
  16589. var y = touch2.pageY - touch1.pageY,
  16590. x = touch2.pageX - touch1.pageX;
  16591. return Math.atan2(y, x) * 180 / Math.PI;
  16592. },
  16593. /**
  16594. * angle to direction define
  16595. * @param {Touch} touch1
  16596. * @param {Touch} touch2
  16597. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  16598. */
  16599. getDirection: function getDirection(touch1, touch2) {
  16600. var x = Math.abs(touch1.pageX - touch2.pageX),
  16601. y = Math.abs(touch1.pageY - touch2.pageY);
  16602. if(x >= y) {
  16603. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  16604. }
  16605. else {
  16606. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  16607. }
  16608. },
  16609. /**
  16610. * calculate the distance between two touches
  16611. * @param {Touch} touch1
  16612. * @param {Touch} touch2
  16613. * @returns {Number} distance
  16614. */
  16615. getDistance: function getDistance(touch1, touch2) {
  16616. var x = touch2.pageX - touch1.pageX,
  16617. y = touch2.pageY - touch1.pageY;
  16618. return Math.sqrt((x*x) + (y*y));
  16619. },
  16620. /**
  16621. * calculate the scale factor between two touchLists (fingers)
  16622. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  16623. * @param {Array} start
  16624. * @param {Array} end
  16625. * @returns {Number} scale
  16626. */
  16627. getScale: function getScale(start, end) {
  16628. // need two fingers...
  16629. if(start.length >= 2 && end.length >= 2) {
  16630. return this.getDistance(end[0], end[1]) /
  16631. this.getDistance(start[0], start[1]);
  16632. }
  16633. return 1;
  16634. },
  16635. /**
  16636. * calculate the rotation degrees between two touchLists (fingers)
  16637. * @param {Array} start
  16638. * @param {Array} end
  16639. * @returns {Number} rotation
  16640. */
  16641. getRotation: function getRotation(start, end) {
  16642. // need two fingers
  16643. if(start.length >= 2 && end.length >= 2) {
  16644. return this.getAngle(end[1], end[0]) -
  16645. this.getAngle(start[1], start[0]);
  16646. }
  16647. return 0;
  16648. },
  16649. /**
  16650. * boolean if the direction is vertical
  16651. * @param {String} direction
  16652. * @returns {Boolean} is_vertical
  16653. */
  16654. isVertical: function isVertical(direction) {
  16655. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  16656. },
  16657. /**
  16658. * stop browser default behavior with css props
  16659. * @param {HtmlElement} element
  16660. * @param {Object} css_props
  16661. */
  16662. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  16663. var prop,
  16664. vendors = ['webkit','khtml','moz','ms','o',''];
  16665. if(!css_props || !element.style) {
  16666. return;
  16667. }
  16668. // with css properties for modern browsers
  16669. for(var i = 0; i < vendors.length; i++) {
  16670. for(var p in css_props) {
  16671. if(css_props.hasOwnProperty(p)) {
  16672. prop = p;
  16673. // vender prefix at the property
  16674. if(vendors[i]) {
  16675. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  16676. }
  16677. // set the style
  16678. element.style[prop] = css_props[p];
  16679. }
  16680. }
  16681. }
  16682. // also the disable onselectstart
  16683. if(css_props.userSelect == 'none') {
  16684. element.onselectstart = function() {
  16685. return false;
  16686. };
  16687. }
  16688. }
  16689. };
  16690. Hammer.detection = {
  16691. // contains all registred Hammer.gestures in the correct order
  16692. gestures: [],
  16693. // data of the current Hammer.gesture detection session
  16694. current: null,
  16695. // the previous Hammer.gesture session data
  16696. // is a full clone of the previous gesture.current object
  16697. previous: null,
  16698. // when this becomes true, no gestures are fired
  16699. stopped: false,
  16700. /**
  16701. * start Hammer.gesture detection
  16702. * @param {Hammer.Instance} inst
  16703. * @param {Object} eventData
  16704. */
  16705. startDetect: function startDetect(inst, eventData) {
  16706. // already busy with a Hammer.gesture detection on an element
  16707. if(this.current) {
  16708. return;
  16709. }
  16710. this.stopped = false;
  16711. this.current = {
  16712. inst : inst, // reference to HammerInstance we're working for
  16713. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  16714. lastEvent : false, // last eventData
  16715. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  16716. };
  16717. this.detect(eventData);
  16718. },
  16719. /**
  16720. * Hammer.gesture detection
  16721. * @param {Object} eventData
  16722. * @param {Object} eventData
  16723. */
  16724. detect: function detect(eventData) {
  16725. if(!this.current || this.stopped) {
  16726. return;
  16727. }
  16728. // extend event data with calculations about scale, distance etc
  16729. eventData = this.extendEventData(eventData);
  16730. // instance options
  16731. var inst_options = this.current.inst.options;
  16732. // call Hammer.gesture handlers
  16733. for(var g=0,len=this.gestures.length; g<len; g++) {
  16734. var gesture = this.gestures[g];
  16735. // only when the instance options have enabled this gesture
  16736. if(!this.stopped && inst_options[gesture.name] !== false) {
  16737. // if a handler returns false, we stop with the detection
  16738. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  16739. this.stopDetect();
  16740. break;
  16741. }
  16742. }
  16743. }
  16744. // store as previous event event
  16745. if(this.current) {
  16746. this.current.lastEvent = eventData;
  16747. }
  16748. // endevent, but not the last touch, so dont stop
  16749. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  16750. this.stopDetect();
  16751. }
  16752. return eventData;
  16753. },
  16754. /**
  16755. * clear the Hammer.gesture vars
  16756. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  16757. * to stop other Hammer.gestures from being fired
  16758. */
  16759. stopDetect: function stopDetect() {
  16760. // clone current data to the store as the previous gesture
  16761. // used for the double tap gesture, since this is an other gesture detect session
  16762. this.previous = Hammer.utils.extend({}, this.current);
  16763. // reset the current
  16764. this.current = null;
  16765. // stopped!
  16766. this.stopped = true;
  16767. },
  16768. /**
  16769. * extend eventData for Hammer.gestures
  16770. * @param {Object} ev
  16771. * @returns {Object} ev
  16772. */
  16773. extendEventData: function extendEventData(ev) {
  16774. var startEv = this.current.startEvent;
  16775. // if the touches change, set the new touches over the startEvent touches
  16776. // this because touchevents don't have all the touches on touchstart, or the
  16777. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  16778. // but, sometimes it happens that both fingers are touching at the EXACT same time
  16779. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  16780. // extend 1 level deep to get the touchlist with the touch objects
  16781. startEv.touches = [];
  16782. for(var i=0,len=ev.touches.length; i<len; i++) {
  16783. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  16784. }
  16785. }
  16786. var delta_time = ev.timeStamp - startEv.timeStamp,
  16787. delta_x = ev.center.pageX - startEv.center.pageX,
  16788. delta_y = ev.center.pageY - startEv.center.pageY,
  16789. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  16790. Hammer.utils.extend(ev, {
  16791. deltaTime : delta_time,
  16792. deltaX : delta_x,
  16793. deltaY : delta_y,
  16794. velocityX : velocity.x,
  16795. velocityY : velocity.y,
  16796. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  16797. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  16798. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  16799. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  16800. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  16801. startEvent : startEv
  16802. });
  16803. return ev;
  16804. },
  16805. /**
  16806. * register new gesture
  16807. * @param {Object} gesture object, see gestures.js for documentation
  16808. * @returns {Array} gestures
  16809. */
  16810. register: function register(gesture) {
  16811. // add an enable gesture options if there is no given
  16812. var options = gesture.defaults || {};
  16813. if(options[gesture.name] === undefined) {
  16814. options[gesture.name] = true;
  16815. }
  16816. // extend Hammer default options with the Hammer.gesture options
  16817. Hammer.utils.extend(Hammer.defaults, options, true);
  16818. // set its index
  16819. gesture.index = gesture.index || 1000;
  16820. // add Hammer.gesture to the list
  16821. this.gestures.push(gesture);
  16822. // sort the list by index
  16823. this.gestures.sort(function(a, b) {
  16824. if (a.index < b.index) {
  16825. return -1;
  16826. }
  16827. if (a.index > b.index) {
  16828. return 1;
  16829. }
  16830. return 0;
  16831. });
  16832. return this.gestures;
  16833. }
  16834. };
  16835. Hammer.gestures = Hammer.gestures || {};
  16836. /**
  16837. * Custom gestures
  16838. * ==============================
  16839. *
  16840. * Gesture object
  16841. * --------------------
  16842. * The object structure of a gesture:
  16843. *
  16844. * { name: 'mygesture',
  16845. * index: 1337,
  16846. * defaults: {
  16847. * mygesture_option: true
  16848. * }
  16849. * handler: function(type, ev, inst) {
  16850. * // trigger gesture event
  16851. * inst.trigger(this.name, ev);
  16852. * }
  16853. * }
  16854. * @param {String} name
  16855. * this should be the name of the gesture, lowercase
  16856. * it is also being used to disable/enable the gesture per instance config.
  16857. *
  16858. * @param {Number} [index=1000]
  16859. * the index of the gesture, where it is going to be in the stack of gestures detection
  16860. * like when you build an gesture that depends on the drag gesture, it is a good
  16861. * idea to place it after the index of the drag gesture.
  16862. *
  16863. * @param {Object} [defaults={}]
  16864. * the default settings of the gesture. these are added to the instance settings,
  16865. * and can be overruled per instance. you can also add the name of the gesture,
  16866. * but this is also added by default (and set to true).
  16867. *
  16868. * @param {Function} handler
  16869. * this handles the gesture detection of your custom gesture and receives the
  16870. * following arguments:
  16871. *
  16872. * @param {Object} eventData
  16873. * event data containing the following properties:
  16874. * timeStamp {Number} time the event occurred
  16875. * target {HTMLElement} target element
  16876. * touches {Array} touches (fingers, pointers, mouse) on the screen
  16877. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  16878. * center {Object} center position of the touches. contains pageX and pageY
  16879. * deltaTime {Number} the total time of the touches in the screen
  16880. * deltaX {Number} the delta on x axis we haved moved
  16881. * deltaY {Number} the delta on y axis we haved moved
  16882. * velocityX {Number} the velocity on the x
  16883. * velocityY {Number} the velocity on y
  16884. * angle {Number} the angle we are moving
  16885. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  16886. * distance {Number} the distance we haved moved
  16887. * scale {Number} scaling of the touches, needs 2 touches
  16888. * rotation {Number} rotation of the touches, needs 2 touches *
  16889. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  16890. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  16891. * startEvent {Object} contains the same properties as above,
  16892. * but from the first touch. this is used to calculate
  16893. * distances, deltaTime, scaling etc
  16894. *
  16895. * @param {Hammer.Instance} inst
  16896. * the instance we are doing the detection for. you can get the options from
  16897. * the inst.options object and trigger the gesture event by calling inst.trigger
  16898. *
  16899. *
  16900. * Handle gestures
  16901. * --------------------
  16902. * inside the handler you can get/set Hammer.detection.current. This is the current
  16903. * detection session. It has the following properties
  16904. * @param {String} name
  16905. * contains the name of the gesture we have detected. it has not a real function,
  16906. * only to check in other gestures if something is detected.
  16907. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  16908. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  16909. *
  16910. * @readonly
  16911. * @param {Hammer.Instance} inst
  16912. * the instance we do the detection for
  16913. *
  16914. * @readonly
  16915. * @param {Object} startEvent
  16916. * contains the properties of the first gesture detection in this session.
  16917. * Used for calculations about timing, distance, etc.
  16918. *
  16919. * @readonly
  16920. * @param {Object} lastEvent
  16921. * contains all the properties of the last gesture detect in this session.
  16922. *
  16923. * after the gesture detection session has been completed (user has released the screen)
  16924. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  16925. * this is usefull for gestures like doubletap, where you need to know if the
  16926. * previous gesture was a tap
  16927. *
  16928. * options that have been set by the instance can be received by calling inst.options
  16929. *
  16930. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  16931. * The first param is the name of your gesture, the second the event argument
  16932. *
  16933. *
  16934. * Register gestures
  16935. * --------------------
  16936. * When an gesture is added to the Hammer.gestures object, it is auto registered
  16937. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  16938. * manually and pass your gesture object as a param
  16939. *
  16940. */
  16941. /**
  16942. * Hold
  16943. * Touch stays at the same place for x time
  16944. * @events hold
  16945. */
  16946. Hammer.gestures.Hold = {
  16947. name: 'hold',
  16948. index: 10,
  16949. defaults: {
  16950. hold_timeout : 500,
  16951. hold_threshold : 1
  16952. },
  16953. timer: null,
  16954. handler: function holdGesture(ev, inst) {
  16955. switch(ev.eventType) {
  16956. case Hammer.EVENT_START:
  16957. // clear any running timers
  16958. clearTimeout(this.timer);
  16959. // set the gesture so we can check in the timeout if it still is
  16960. Hammer.detection.current.name = this.name;
  16961. // set timer and if after the timeout it still is hold,
  16962. // we trigger the hold event
  16963. this.timer = setTimeout(function() {
  16964. if(Hammer.detection.current.name == 'hold') {
  16965. inst.trigger('hold', ev);
  16966. }
  16967. }, inst.options.hold_timeout);
  16968. break;
  16969. // when you move or end we clear the timer
  16970. case Hammer.EVENT_MOVE:
  16971. if(ev.distance > inst.options.hold_threshold) {
  16972. clearTimeout(this.timer);
  16973. }
  16974. break;
  16975. case Hammer.EVENT_END:
  16976. clearTimeout(this.timer);
  16977. break;
  16978. }
  16979. }
  16980. };
  16981. /**
  16982. * Tap/DoubleTap
  16983. * Quick touch at a place or double at the same place
  16984. * @events tap, doubletap
  16985. */
  16986. Hammer.gestures.Tap = {
  16987. name: 'tap',
  16988. index: 100,
  16989. defaults: {
  16990. tap_max_touchtime : 250,
  16991. tap_max_distance : 10,
  16992. tap_always : true,
  16993. doubletap_distance : 20,
  16994. doubletap_interval : 300
  16995. },
  16996. handler: function tapGesture(ev, inst) {
  16997. if(ev.eventType == Hammer.EVENT_END) {
  16998. // previous gesture, for the double tap since these are two different gesture detections
  16999. var prev = Hammer.detection.previous,
  17000. did_doubletap = false;
  17001. // when the touchtime is higher then the max touch time
  17002. // or when the moving distance is too much
  17003. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  17004. ev.distance > inst.options.tap_max_distance) {
  17005. return;
  17006. }
  17007. // check if double tap
  17008. if(prev && prev.name == 'tap' &&
  17009. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  17010. ev.distance < inst.options.doubletap_distance) {
  17011. inst.trigger('doubletap', ev);
  17012. did_doubletap = true;
  17013. }
  17014. // do a single tap
  17015. if(!did_doubletap || inst.options.tap_always) {
  17016. Hammer.detection.current.name = 'tap';
  17017. inst.trigger(Hammer.detection.current.name, ev);
  17018. }
  17019. }
  17020. }
  17021. };
  17022. /**
  17023. * Swipe
  17024. * triggers swipe events when the end velocity is above the threshold
  17025. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  17026. */
  17027. Hammer.gestures.Swipe = {
  17028. name: 'swipe',
  17029. index: 40,
  17030. defaults: {
  17031. // set 0 for unlimited, but this can conflict with transform
  17032. swipe_max_touches : 1,
  17033. swipe_velocity : 0.7
  17034. },
  17035. handler: function swipeGesture(ev, inst) {
  17036. if(ev.eventType == Hammer.EVENT_END) {
  17037. // max touches
  17038. if(inst.options.swipe_max_touches > 0 &&
  17039. ev.touches.length > inst.options.swipe_max_touches) {
  17040. return;
  17041. }
  17042. // when the distance we moved is too small we skip this gesture
  17043. // or we can be already in dragging
  17044. if(ev.velocityX > inst.options.swipe_velocity ||
  17045. ev.velocityY > inst.options.swipe_velocity) {
  17046. // trigger swipe events
  17047. inst.trigger(this.name, ev);
  17048. inst.trigger(this.name + ev.direction, ev);
  17049. }
  17050. }
  17051. }
  17052. };
  17053. /**
  17054. * Drag
  17055. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  17056. * moving left and right is a good practice. When all the drag events are blocking
  17057. * you disable scrolling on that area.
  17058. * @events drag, drapleft, dragright, dragup, dragdown
  17059. */
  17060. Hammer.gestures.Drag = {
  17061. name: 'drag',
  17062. index: 50,
  17063. defaults: {
  17064. drag_min_distance : 10,
  17065. // set 0 for unlimited, but this can conflict with transform
  17066. drag_max_touches : 1,
  17067. // prevent default browser behavior when dragging occurs
  17068. // be careful with it, it makes the element a blocking element
  17069. // when you are using the drag gesture, it is a good practice to set this true
  17070. drag_block_horizontal : false,
  17071. drag_block_vertical : false,
  17072. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  17073. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  17074. drag_lock_to_axis : false,
  17075. // drag lock only kicks in when distance > drag_lock_min_distance
  17076. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  17077. drag_lock_min_distance : 25
  17078. },
  17079. triggered: false,
  17080. handler: function dragGesture(ev, inst) {
  17081. // current gesture isnt drag, but dragged is true
  17082. // this means an other gesture is busy. now call dragend
  17083. if(Hammer.detection.current.name != this.name && this.triggered) {
  17084. inst.trigger(this.name +'end', ev);
  17085. this.triggered = false;
  17086. return;
  17087. }
  17088. // max touches
  17089. if(inst.options.drag_max_touches > 0 &&
  17090. ev.touches.length > inst.options.drag_max_touches) {
  17091. return;
  17092. }
  17093. switch(ev.eventType) {
  17094. case Hammer.EVENT_START:
  17095. this.triggered = false;
  17096. break;
  17097. case Hammer.EVENT_MOVE:
  17098. // when the distance we moved is too small we skip this gesture
  17099. // or we can be already in dragging
  17100. if(ev.distance < inst.options.drag_min_distance &&
  17101. Hammer.detection.current.name != this.name) {
  17102. return;
  17103. }
  17104. // we are dragging!
  17105. Hammer.detection.current.name = this.name;
  17106. // lock drag to axis?
  17107. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  17108. ev.drag_locked_to_axis = true;
  17109. }
  17110. var last_direction = Hammer.detection.current.lastEvent.direction;
  17111. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  17112. // keep direction on the axis that the drag gesture started on
  17113. if(Hammer.utils.isVertical(last_direction)) {
  17114. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  17115. }
  17116. else {
  17117. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  17118. }
  17119. }
  17120. // first time, trigger dragstart event
  17121. if(!this.triggered) {
  17122. inst.trigger(this.name +'start', ev);
  17123. this.triggered = true;
  17124. }
  17125. // trigger normal event
  17126. inst.trigger(this.name, ev);
  17127. // direction event, like dragdown
  17128. inst.trigger(this.name + ev.direction, ev);
  17129. // block the browser events
  17130. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  17131. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  17132. ev.preventDefault();
  17133. }
  17134. break;
  17135. case Hammer.EVENT_END:
  17136. // trigger dragend
  17137. if(this.triggered) {
  17138. inst.trigger(this.name +'end', ev);
  17139. }
  17140. this.triggered = false;
  17141. break;
  17142. }
  17143. }
  17144. };
  17145. /**
  17146. * Transform
  17147. * User want to scale or rotate with 2 fingers
  17148. * @events transform, pinch, pinchin, pinchout, rotate
  17149. */
  17150. Hammer.gestures.Transform = {
  17151. name: 'transform',
  17152. index: 45,
  17153. defaults: {
  17154. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  17155. transform_min_scale : 0.01,
  17156. // rotation in degrees
  17157. transform_min_rotation : 1,
  17158. // prevent default browser behavior when two touches are on the screen
  17159. // but it makes the element a blocking element
  17160. // when you are using the transform gesture, it is a good practice to set this true
  17161. transform_always_block : false
  17162. },
  17163. triggered: false,
  17164. handler: function transformGesture(ev, inst) {
  17165. // current gesture isnt drag, but dragged is true
  17166. // this means an other gesture is busy. now call dragend
  17167. if(Hammer.detection.current.name != this.name && this.triggered) {
  17168. inst.trigger(this.name +'end', ev);
  17169. this.triggered = false;
  17170. return;
  17171. }
  17172. // atleast multitouch
  17173. if(ev.touches.length < 2) {
  17174. return;
  17175. }
  17176. // prevent default when two fingers are on the screen
  17177. if(inst.options.transform_always_block) {
  17178. ev.preventDefault();
  17179. }
  17180. switch(ev.eventType) {
  17181. case Hammer.EVENT_START:
  17182. this.triggered = false;
  17183. break;
  17184. case Hammer.EVENT_MOVE:
  17185. var scale_threshold = Math.abs(1-ev.scale);
  17186. var rotation_threshold = Math.abs(ev.rotation);
  17187. // when the distance we moved is too small we skip this gesture
  17188. // or we can be already in dragging
  17189. if(scale_threshold < inst.options.transform_min_scale &&
  17190. rotation_threshold < inst.options.transform_min_rotation) {
  17191. return;
  17192. }
  17193. // we are transforming!
  17194. Hammer.detection.current.name = this.name;
  17195. // first time, trigger dragstart event
  17196. if(!this.triggered) {
  17197. inst.trigger(this.name +'start', ev);
  17198. this.triggered = true;
  17199. }
  17200. inst.trigger(this.name, ev); // basic transform event
  17201. // trigger rotate event
  17202. if(rotation_threshold > inst.options.transform_min_rotation) {
  17203. inst.trigger('rotate', ev);
  17204. }
  17205. // trigger pinch event
  17206. if(scale_threshold > inst.options.transform_min_scale) {
  17207. inst.trigger('pinch', ev);
  17208. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  17209. }
  17210. break;
  17211. case Hammer.EVENT_END:
  17212. // trigger dragend
  17213. if(this.triggered) {
  17214. inst.trigger(this.name +'end', ev);
  17215. }
  17216. this.triggered = false;
  17217. break;
  17218. }
  17219. }
  17220. };
  17221. /**
  17222. * Touch
  17223. * Called as first, tells the user has touched the screen
  17224. * @events touch
  17225. */
  17226. Hammer.gestures.Touch = {
  17227. name: 'touch',
  17228. index: -Infinity,
  17229. defaults: {
  17230. // call preventDefault at touchstart, and makes the element blocking by
  17231. // disabling the scrolling of the page, but it improves gestures like
  17232. // transforming and dragging.
  17233. // be careful with using this, it can be very annoying for users to be stuck
  17234. // on the page
  17235. prevent_default: false,
  17236. // disable mouse events, so only touch (or pen!) input triggers events
  17237. prevent_mouseevents: false
  17238. },
  17239. handler: function touchGesture(ev, inst) {
  17240. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  17241. ev.stopDetect();
  17242. return;
  17243. }
  17244. if(inst.options.prevent_default) {
  17245. ev.preventDefault();
  17246. }
  17247. if(ev.eventType == Hammer.EVENT_START) {
  17248. inst.trigger(this.name, ev);
  17249. }
  17250. }
  17251. };
  17252. /**
  17253. * Release
  17254. * Called as last, tells the user has released the screen
  17255. * @events release
  17256. */
  17257. Hammer.gestures.Release = {
  17258. name: 'release',
  17259. index: Infinity,
  17260. handler: function releaseGesture(ev, inst) {
  17261. if(ev.eventType == Hammer.EVENT_END) {
  17262. inst.trigger(this.name, ev);
  17263. }
  17264. }
  17265. };
  17266. // node export
  17267. if(typeof module === 'object' && typeof module.exports === 'object'){
  17268. module.exports = Hammer;
  17269. }
  17270. // just window export
  17271. else {
  17272. window.Hammer = Hammer;
  17273. // requireJS module definition
  17274. if(typeof window.define === 'function' && window.define.amd) {
  17275. window.define('hammer', [], function() {
  17276. return Hammer;
  17277. });
  17278. }
  17279. }
  17280. })(this);
  17281. },{}],4:[function(require,module,exports){
  17282. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  17283. //! version : 2.6.0
  17284. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  17285. //! license : MIT
  17286. //! momentjs.com
  17287. (function (undefined) {
  17288. /************************************
  17289. Constants
  17290. ************************************/
  17291. var moment,
  17292. VERSION = "2.6.0",
  17293. // the global-scope this is NOT the global object in Node.js
  17294. globalScope = typeof global !== 'undefined' ? global : this,
  17295. oldGlobalMoment,
  17296. round = Math.round,
  17297. i,
  17298. YEAR = 0,
  17299. MONTH = 1,
  17300. DATE = 2,
  17301. HOUR = 3,
  17302. MINUTE = 4,
  17303. SECOND = 5,
  17304. MILLISECOND = 6,
  17305. // internal storage for language config files
  17306. languages = {},
  17307. // moment internal properties
  17308. momentProperties = {
  17309. _isAMomentObject: null,
  17310. _i : null,
  17311. _f : null,
  17312. _l : null,
  17313. _strict : null,
  17314. _isUTC : null,
  17315. _offset : null, // optional. Combine with _isUTC
  17316. _pf : null,
  17317. _lang : null // optional
  17318. },
  17319. // check for nodeJS
  17320. hasModule = (typeof module !== 'undefined' && module.exports),
  17321. // ASP.NET json date format regex
  17322. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  17323. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  17324. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  17325. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  17326. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  17327. // format tokens
  17328. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
  17329. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  17330. // parsing token regexes
  17331. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  17332. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  17333. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  17334. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  17335. parseTokenDigits = /\d+/, // nonzero number of digits
  17336. 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.
  17337. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  17338. parseTokenT = /T/i, // T (ISO separator)
  17339. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  17340. parseTokenOrdinal = /\d{1,2}/,
  17341. //strict parsing regexes
  17342. parseTokenOneDigit = /\d/, // 0 - 9
  17343. parseTokenTwoDigits = /\d\d/, // 00 - 99
  17344. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  17345. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  17346. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  17347. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  17348. // iso 8601 regex
  17349. // 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)
  17350. 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)?)?$/,
  17351. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  17352. isoDates = [
  17353. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  17354. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  17355. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  17356. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  17357. ['YYYY-DDD', /\d{4}-\d{3}/]
  17358. ],
  17359. // iso time formats and regexes
  17360. isoTimes = [
  17361. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  17362. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  17363. ['HH:mm', /(T| )\d\d:\d\d/],
  17364. ['HH', /(T| )\d\d/]
  17365. ],
  17366. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  17367. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  17368. // getter and setter names
  17369. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  17370. unitMillisecondFactors = {
  17371. 'Milliseconds' : 1,
  17372. 'Seconds' : 1e3,
  17373. 'Minutes' : 6e4,
  17374. 'Hours' : 36e5,
  17375. 'Days' : 864e5,
  17376. 'Months' : 2592e6,
  17377. 'Years' : 31536e6
  17378. },
  17379. unitAliases = {
  17380. ms : 'millisecond',
  17381. s : 'second',
  17382. m : 'minute',
  17383. h : 'hour',
  17384. d : 'day',
  17385. D : 'date',
  17386. w : 'week',
  17387. W : 'isoWeek',
  17388. M : 'month',
  17389. Q : 'quarter',
  17390. y : 'year',
  17391. DDD : 'dayOfYear',
  17392. e : 'weekday',
  17393. E : 'isoWeekday',
  17394. gg: 'weekYear',
  17395. GG: 'isoWeekYear'
  17396. },
  17397. camelFunctions = {
  17398. dayofyear : 'dayOfYear',
  17399. isoweekday : 'isoWeekday',
  17400. isoweek : 'isoWeek',
  17401. weekyear : 'weekYear',
  17402. isoweekyear : 'isoWeekYear'
  17403. },
  17404. // format function strings
  17405. formatFunctions = {},
  17406. // tokens to ordinalize and pad
  17407. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  17408. paddedTokens = 'M D H h m s w W'.split(' '),
  17409. formatTokenFunctions = {
  17410. M : function () {
  17411. return this.month() + 1;
  17412. },
  17413. MMM : function (format) {
  17414. return this.lang().monthsShort(this, format);
  17415. },
  17416. MMMM : function (format) {
  17417. return this.lang().months(this, format);
  17418. },
  17419. D : function () {
  17420. return this.date();
  17421. },
  17422. DDD : function () {
  17423. return this.dayOfYear();
  17424. },
  17425. d : function () {
  17426. return this.day();
  17427. },
  17428. dd : function (format) {
  17429. return this.lang().weekdaysMin(this, format);
  17430. },
  17431. ddd : function (format) {
  17432. return this.lang().weekdaysShort(this, format);
  17433. },
  17434. dddd : function (format) {
  17435. return this.lang().weekdays(this, format);
  17436. },
  17437. w : function () {
  17438. return this.week();
  17439. },
  17440. W : function () {
  17441. return this.isoWeek();
  17442. },
  17443. YY : function () {
  17444. return leftZeroFill(this.year() % 100, 2);
  17445. },
  17446. YYYY : function () {
  17447. return leftZeroFill(this.year(), 4);
  17448. },
  17449. YYYYY : function () {
  17450. return leftZeroFill(this.year(), 5);
  17451. },
  17452. YYYYYY : function () {
  17453. var y = this.year(), sign = y >= 0 ? '+' : '-';
  17454. return sign + leftZeroFill(Math.abs(y), 6);
  17455. },
  17456. gg : function () {
  17457. return leftZeroFill(this.weekYear() % 100, 2);
  17458. },
  17459. gggg : function () {
  17460. return leftZeroFill(this.weekYear(), 4);
  17461. },
  17462. ggggg : function () {
  17463. return leftZeroFill(this.weekYear(), 5);
  17464. },
  17465. GG : function () {
  17466. return leftZeroFill(this.isoWeekYear() % 100, 2);
  17467. },
  17468. GGGG : function () {
  17469. return leftZeroFill(this.isoWeekYear(), 4);
  17470. },
  17471. GGGGG : function () {
  17472. return leftZeroFill(this.isoWeekYear(), 5);
  17473. },
  17474. e : function () {
  17475. return this.weekday();
  17476. },
  17477. E : function () {
  17478. return this.isoWeekday();
  17479. },
  17480. a : function () {
  17481. return this.lang().meridiem(this.hours(), this.minutes(), true);
  17482. },
  17483. A : function () {
  17484. return this.lang().meridiem(this.hours(), this.minutes(), false);
  17485. },
  17486. H : function () {
  17487. return this.hours();
  17488. },
  17489. h : function () {
  17490. return this.hours() % 12 || 12;
  17491. },
  17492. m : function () {
  17493. return this.minutes();
  17494. },
  17495. s : function () {
  17496. return this.seconds();
  17497. },
  17498. S : function () {
  17499. return toInt(this.milliseconds() / 100);
  17500. },
  17501. SS : function () {
  17502. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  17503. },
  17504. SSS : function () {
  17505. return leftZeroFill(this.milliseconds(), 3);
  17506. },
  17507. SSSS : function () {
  17508. return leftZeroFill(this.milliseconds(), 3);
  17509. },
  17510. Z : function () {
  17511. var a = -this.zone(),
  17512. b = "+";
  17513. if (a < 0) {
  17514. a = -a;
  17515. b = "-";
  17516. }
  17517. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  17518. },
  17519. ZZ : function () {
  17520. var a = -this.zone(),
  17521. b = "+";
  17522. if (a < 0) {
  17523. a = -a;
  17524. b = "-";
  17525. }
  17526. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  17527. },
  17528. z : function () {
  17529. return this.zoneAbbr();
  17530. },
  17531. zz : function () {
  17532. return this.zoneName();
  17533. },
  17534. X : function () {
  17535. return this.unix();
  17536. },
  17537. Q : function () {
  17538. return this.quarter();
  17539. }
  17540. },
  17541. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  17542. function defaultParsingFlags() {
  17543. // We need to deep clone this object, and es5 standard is not very
  17544. // helpful.
  17545. return {
  17546. empty : false,
  17547. unusedTokens : [],
  17548. unusedInput : [],
  17549. overflow : -2,
  17550. charsLeftOver : 0,
  17551. nullInput : false,
  17552. invalidMonth : null,
  17553. invalidFormat : false,
  17554. userInvalidated : false,
  17555. iso: false
  17556. };
  17557. }
  17558. function deprecate(msg, fn) {
  17559. var firstTime = true;
  17560. function printMsg() {
  17561. if (moment.suppressDeprecationWarnings === false &&
  17562. typeof console !== 'undefined' && console.warn) {
  17563. console.warn("Deprecation warning: " + msg);
  17564. }
  17565. }
  17566. return extend(function () {
  17567. if (firstTime) {
  17568. printMsg();
  17569. firstTime = false;
  17570. }
  17571. return fn.apply(this, arguments);
  17572. }, fn);
  17573. }
  17574. function padToken(func, count) {
  17575. return function (a) {
  17576. return leftZeroFill(func.call(this, a), count);
  17577. };
  17578. }
  17579. function ordinalizeToken(func, period) {
  17580. return function (a) {
  17581. return this.lang().ordinal(func.call(this, a), period);
  17582. };
  17583. }
  17584. while (ordinalizeTokens.length) {
  17585. i = ordinalizeTokens.pop();
  17586. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  17587. }
  17588. while (paddedTokens.length) {
  17589. i = paddedTokens.pop();
  17590. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  17591. }
  17592. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  17593. /************************************
  17594. Constructors
  17595. ************************************/
  17596. function Language() {
  17597. }
  17598. // Moment prototype object
  17599. function Moment(config) {
  17600. checkOverflow(config);
  17601. extend(this, config);
  17602. }
  17603. // Duration Constructor
  17604. function Duration(duration) {
  17605. var normalizedInput = normalizeObjectUnits(duration),
  17606. years = normalizedInput.year || 0,
  17607. quarters = normalizedInput.quarter || 0,
  17608. months = normalizedInput.month || 0,
  17609. weeks = normalizedInput.week || 0,
  17610. days = normalizedInput.day || 0,
  17611. hours = normalizedInput.hour || 0,
  17612. minutes = normalizedInput.minute || 0,
  17613. seconds = normalizedInput.second || 0,
  17614. milliseconds = normalizedInput.millisecond || 0;
  17615. // representation for dateAddRemove
  17616. this._milliseconds = +milliseconds +
  17617. seconds * 1e3 + // 1000
  17618. minutes * 6e4 + // 1000 * 60
  17619. hours * 36e5; // 1000 * 60 * 60
  17620. // Because of dateAddRemove treats 24 hours as different from a
  17621. // day when working around DST, we need to store them separately
  17622. this._days = +days +
  17623. weeks * 7;
  17624. // It is impossible translate months into days without knowing
  17625. // which months you are are talking about, so we have to store
  17626. // it separately.
  17627. this._months = +months +
  17628. quarters * 3 +
  17629. years * 12;
  17630. this._data = {};
  17631. this._bubble();
  17632. }
  17633. /************************************
  17634. Helpers
  17635. ************************************/
  17636. function extend(a, b) {
  17637. for (var i in b) {
  17638. if (b.hasOwnProperty(i)) {
  17639. a[i] = b[i];
  17640. }
  17641. }
  17642. if (b.hasOwnProperty("toString")) {
  17643. a.toString = b.toString;
  17644. }
  17645. if (b.hasOwnProperty("valueOf")) {
  17646. a.valueOf = b.valueOf;
  17647. }
  17648. return a;
  17649. }
  17650. function cloneMoment(m) {
  17651. var result = {}, i;
  17652. for (i in m) {
  17653. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  17654. result[i] = m[i];
  17655. }
  17656. }
  17657. return result;
  17658. }
  17659. function absRound(number) {
  17660. if (number < 0) {
  17661. return Math.ceil(number);
  17662. } else {
  17663. return Math.floor(number);
  17664. }
  17665. }
  17666. // left zero fill a number
  17667. // see http://jsperf.com/left-zero-filling for performance comparison
  17668. function leftZeroFill(number, targetLength, forceSign) {
  17669. var output = '' + Math.abs(number),
  17670. sign = number >= 0;
  17671. while (output.length < targetLength) {
  17672. output = '0' + output;
  17673. }
  17674. return (sign ? (forceSign ? '+' : '') : '-') + output;
  17675. }
  17676. // helper function for _.addTime and _.subtractTime
  17677. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  17678. var milliseconds = duration._milliseconds,
  17679. days = duration._days,
  17680. months = duration._months;
  17681. updateOffset = updateOffset == null ? true : updateOffset;
  17682. if (milliseconds) {
  17683. mom._d.setTime(+mom._d + milliseconds * isAdding);
  17684. }
  17685. if (days) {
  17686. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  17687. }
  17688. if (months) {
  17689. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  17690. }
  17691. if (updateOffset) {
  17692. moment.updateOffset(mom, days || months);
  17693. }
  17694. }
  17695. // check if is an array
  17696. function isArray(input) {
  17697. return Object.prototype.toString.call(input) === '[object Array]';
  17698. }
  17699. function isDate(input) {
  17700. return Object.prototype.toString.call(input) === '[object Date]' ||
  17701. input instanceof Date;
  17702. }
  17703. // compare two arrays, return the number of differences
  17704. function compareArrays(array1, array2, dontConvert) {
  17705. var len = Math.min(array1.length, array2.length),
  17706. lengthDiff = Math.abs(array1.length - array2.length),
  17707. diffs = 0,
  17708. i;
  17709. for (i = 0; i < len; i++) {
  17710. if ((dontConvert && array1[i] !== array2[i]) ||
  17711. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  17712. diffs++;
  17713. }
  17714. }
  17715. return diffs + lengthDiff;
  17716. }
  17717. function normalizeUnits(units) {
  17718. if (units) {
  17719. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  17720. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  17721. }
  17722. return units;
  17723. }
  17724. function normalizeObjectUnits(inputObject) {
  17725. var normalizedInput = {},
  17726. normalizedProp,
  17727. prop;
  17728. for (prop in inputObject) {
  17729. if (inputObject.hasOwnProperty(prop)) {
  17730. normalizedProp = normalizeUnits(prop);
  17731. if (normalizedProp) {
  17732. normalizedInput[normalizedProp] = inputObject[prop];
  17733. }
  17734. }
  17735. }
  17736. return normalizedInput;
  17737. }
  17738. function makeList(field) {
  17739. var count, setter;
  17740. if (field.indexOf('week') === 0) {
  17741. count = 7;
  17742. setter = 'day';
  17743. }
  17744. else if (field.indexOf('month') === 0) {
  17745. count = 12;
  17746. setter = 'month';
  17747. }
  17748. else {
  17749. return;
  17750. }
  17751. moment[field] = function (format, index) {
  17752. var i, getter,
  17753. method = moment.fn._lang[field],
  17754. results = [];
  17755. if (typeof format === 'number') {
  17756. index = format;
  17757. format = undefined;
  17758. }
  17759. getter = function (i) {
  17760. var m = moment().utc().set(setter, i);
  17761. return method.call(moment.fn._lang, m, format || '');
  17762. };
  17763. if (index != null) {
  17764. return getter(index);
  17765. }
  17766. else {
  17767. for (i = 0; i < count; i++) {
  17768. results.push(getter(i));
  17769. }
  17770. return results;
  17771. }
  17772. };
  17773. }
  17774. function toInt(argumentForCoercion) {
  17775. var coercedNumber = +argumentForCoercion,
  17776. value = 0;
  17777. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  17778. if (coercedNumber >= 0) {
  17779. value = Math.floor(coercedNumber);
  17780. } else {
  17781. value = Math.ceil(coercedNumber);
  17782. }
  17783. }
  17784. return value;
  17785. }
  17786. function daysInMonth(year, month) {
  17787. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  17788. }
  17789. function weeksInYear(year, dow, doy) {
  17790. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  17791. }
  17792. function daysInYear(year) {
  17793. return isLeapYear(year) ? 366 : 365;
  17794. }
  17795. function isLeapYear(year) {
  17796. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  17797. }
  17798. function checkOverflow(m) {
  17799. var overflow;
  17800. if (m._a && m._pf.overflow === -2) {
  17801. overflow =
  17802. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  17803. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  17804. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  17805. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  17806. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  17807. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  17808. -1;
  17809. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  17810. overflow = DATE;
  17811. }
  17812. m._pf.overflow = overflow;
  17813. }
  17814. }
  17815. function isValid(m) {
  17816. if (m._isValid == null) {
  17817. m._isValid = !isNaN(m._d.getTime()) &&
  17818. m._pf.overflow < 0 &&
  17819. !m._pf.empty &&
  17820. !m._pf.invalidMonth &&
  17821. !m._pf.nullInput &&
  17822. !m._pf.invalidFormat &&
  17823. !m._pf.userInvalidated;
  17824. if (m._strict) {
  17825. m._isValid = m._isValid &&
  17826. m._pf.charsLeftOver === 0 &&
  17827. m._pf.unusedTokens.length === 0;
  17828. }
  17829. }
  17830. return m._isValid;
  17831. }
  17832. function normalizeLanguage(key) {
  17833. return key ? key.toLowerCase().replace('_', '-') : key;
  17834. }
  17835. // Return a moment from input, that is local/utc/zone equivalent to model.
  17836. function makeAs(input, model) {
  17837. return model._isUTC ? moment(input).zone(model._offset || 0) :
  17838. moment(input).local();
  17839. }
  17840. /************************************
  17841. Languages
  17842. ************************************/
  17843. extend(Language.prototype, {
  17844. set : function (config) {
  17845. var prop, i;
  17846. for (i in config) {
  17847. prop = config[i];
  17848. if (typeof prop === 'function') {
  17849. this[i] = prop;
  17850. } else {
  17851. this['_' + i] = prop;
  17852. }
  17853. }
  17854. },
  17855. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  17856. months : function (m) {
  17857. return this._months[m.month()];
  17858. },
  17859. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  17860. monthsShort : function (m) {
  17861. return this._monthsShort[m.month()];
  17862. },
  17863. monthsParse : function (monthName) {
  17864. var i, mom, regex;
  17865. if (!this._monthsParse) {
  17866. this._monthsParse = [];
  17867. }
  17868. for (i = 0; i < 12; i++) {
  17869. // make the regex if we don't have it already
  17870. if (!this._monthsParse[i]) {
  17871. mom = moment.utc([2000, i]);
  17872. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  17873. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17874. }
  17875. // test the regex
  17876. if (this._monthsParse[i].test(monthName)) {
  17877. return i;
  17878. }
  17879. }
  17880. },
  17881. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  17882. weekdays : function (m) {
  17883. return this._weekdays[m.day()];
  17884. },
  17885. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  17886. weekdaysShort : function (m) {
  17887. return this._weekdaysShort[m.day()];
  17888. },
  17889. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  17890. weekdaysMin : function (m) {
  17891. return this._weekdaysMin[m.day()];
  17892. },
  17893. weekdaysParse : function (weekdayName) {
  17894. var i, mom, regex;
  17895. if (!this._weekdaysParse) {
  17896. this._weekdaysParse = [];
  17897. }
  17898. for (i = 0; i < 7; i++) {
  17899. // make the regex if we don't have it already
  17900. if (!this._weekdaysParse[i]) {
  17901. mom = moment([2000, 1]).day(i);
  17902. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  17903. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17904. }
  17905. // test the regex
  17906. if (this._weekdaysParse[i].test(weekdayName)) {
  17907. return i;
  17908. }
  17909. }
  17910. },
  17911. _longDateFormat : {
  17912. LT : "h:mm A",
  17913. L : "MM/DD/YYYY",
  17914. LL : "MMMM D YYYY",
  17915. LLL : "MMMM D YYYY LT",
  17916. LLLL : "dddd, MMMM D YYYY LT"
  17917. },
  17918. longDateFormat : function (key) {
  17919. var output = this._longDateFormat[key];
  17920. if (!output && this._longDateFormat[key.toUpperCase()]) {
  17921. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  17922. return val.slice(1);
  17923. });
  17924. this._longDateFormat[key] = output;
  17925. }
  17926. return output;
  17927. },
  17928. isPM : function (input) {
  17929. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  17930. // Using charAt should be more compatible.
  17931. return ((input + '').toLowerCase().charAt(0) === 'p');
  17932. },
  17933. _meridiemParse : /[ap]\.?m?\.?/i,
  17934. meridiem : function (hours, minutes, isLower) {
  17935. if (hours > 11) {
  17936. return isLower ? 'pm' : 'PM';
  17937. } else {
  17938. return isLower ? 'am' : 'AM';
  17939. }
  17940. },
  17941. _calendar : {
  17942. sameDay : '[Today at] LT',
  17943. nextDay : '[Tomorrow at] LT',
  17944. nextWeek : 'dddd [at] LT',
  17945. lastDay : '[Yesterday at] LT',
  17946. lastWeek : '[Last] dddd [at] LT',
  17947. sameElse : 'L'
  17948. },
  17949. calendar : function (key, mom) {
  17950. var output = this._calendar[key];
  17951. return typeof output === 'function' ? output.apply(mom) : output;
  17952. },
  17953. _relativeTime : {
  17954. future : "in %s",
  17955. past : "%s ago",
  17956. s : "a few seconds",
  17957. m : "a minute",
  17958. mm : "%d minutes",
  17959. h : "an hour",
  17960. hh : "%d hours",
  17961. d : "a day",
  17962. dd : "%d days",
  17963. M : "a month",
  17964. MM : "%d months",
  17965. y : "a year",
  17966. yy : "%d years"
  17967. },
  17968. relativeTime : function (number, withoutSuffix, string, isFuture) {
  17969. var output = this._relativeTime[string];
  17970. return (typeof output === 'function') ?
  17971. output(number, withoutSuffix, string, isFuture) :
  17972. output.replace(/%d/i, number);
  17973. },
  17974. pastFuture : function (diff, output) {
  17975. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  17976. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  17977. },
  17978. ordinal : function (number) {
  17979. return this._ordinal.replace("%d", number);
  17980. },
  17981. _ordinal : "%d",
  17982. preparse : function (string) {
  17983. return string;
  17984. },
  17985. postformat : function (string) {
  17986. return string;
  17987. },
  17988. week : function (mom) {
  17989. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  17990. },
  17991. _week : {
  17992. dow : 0, // Sunday is the first day of the week.
  17993. doy : 6 // The week that contains Jan 1st is the first week of the year.
  17994. },
  17995. _invalidDate: 'Invalid date',
  17996. invalidDate: function () {
  17997. return this._invalidDate;
  17998. }
  17999. });
  18000. // Loads a language definition into the `languages` cache. The function
  18001. // takes a key and optionally values. If not in the browser and no values
  18002. // are provided, it will load the language file module. As a convenience,
  18003. // this function also returns the language values.
  18004. function loadLang(key, values) {
  18005. values.abbr = key;
  18006. if (!languages[key]) {
  18007. languages[key] = new Language();
  18008. }
  18009. languages[key].set(values);
  18010. return languages[key];
  18011. }
  18012. // Remove a language from the `languages` cache. Mostly useful in tests.
  18013. function unloadLang(key) {
  18014. delete languages[key];
  18015. }
  18016. // Determines which language definition to use and returns it.
  18017. //
  18018. // With no parameters, it will return the global language. If you
  18019. // pass in a language key, such as 'en', it will return the
  18020. // definition for 'en', so long as 'en' has already been loaded using
  18021. // moment.lang.
  18022. function getLangDefinition(key) {
  18023. var i = 0, j, lang, next, split,
  18024. get = function (k) {
  18025. if (!languages[k] && hasModule) {
  18026. try {
  18027. require('./lang/' + k);
  18028. } catch (e) { }
  18029. }
  18030. return languages[k];
  18031. };
  18032. if (!key) {
  18033. return moment.fn._lang;
  18034. }
  18035. if (!isArray(key)) {
  18036. //short-circuit everything else
  18037. lang = get(key);
  18038. if (lang) {
  18039. return lang;
  18040. }
  18041. key = [key];
  18042. }
  18043. //pick the language from the array
  18044. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  18045. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  18046. while (i < key.length) {
  18047. split = normalizeLanguage(key[i]).split('-');
  18048. j = split.length;
  18049. next = normalizeLanguage(key[i + 1]);
  18050. next = next ? next.split('-') : null;
  18051. while (j > 0) {
  18052. lang = get(split.slice(0, j).join('-'));
  18053. if (lang) {
  18054. return lang;
  18055. }
  18056. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  18057. //the next array item is better than a shallower substring of this one
  18058. break;
  18059. }
  18060. j--;
  18061. }
  18062. i++;
  18063. }
  18064. return moment.fn._lang;
  18065. }
  18066. /************************************
  18067. Formatting
  18068. ************************************/
  18069. function removeFormattingTokens(input) {
  18070. if (input.match(/\[[\s\S]/)) {
  18071. return input.replace(/^\[|\]$/g, "");
  18072. }
  18073. return input.replace(/\\/g, "");
  18074. }
  18075. function makeFormatFunction(format) {
  18076. var array = format.match(formattingTokens), i, length;
  18077. for (i = 0, length = array.length; i < length; i++) {
  18078. if (formatTokenFunctions[array[i]]) {
  18079. array[i] = formatTokenFunctions[array[i]];
  18080. } else {
  18081. array[i] = removeFormattingTokens(array[i]);
  18082. }
  18083. }
  18084. return function (mom) {
  18085. var output = "";
  18086. for (i = 0; i < length; i++) {
  18087. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  18088. }
  18089. return output;
  18090. };
  18091. }
  18092. // format date using native date object
  18093. function formatMoment(m, format) {
  18094. if (!m.isValid()) {
  18095. return m.lang().invalidDate();
  18096. }
  18097. format = expandFormat(format, m.lang());
  18098. if (!formatFunctions[format]) {
  18099. formatFunctions[format] = makeFormatFunction(format);
  18100. }
  18101. return formatFunctions[format](m);
  18102. }
  18103. function expandFormat(format, lang) {
  18104. var i = 5;
  18105. function replaceLongDateFormatTokens(input) {
  18106. return lang.longDateFormat(input) || input;
  18107. }
  18108. localFormattingTokens.lastIndex = 0;
  18109. while (i >= 0 && localFormattingTokens.test(format)) {
  18110. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  18111. localFormattingTokens.lastIndex = 0;
  18112. i -= 1;
  18113. }
  18114. return format;
  18115. }
  18116. /************************************
  18117. Parsing
  18118. ************************************/
  18119. // get the regex to find the next token
  18120. function getParseRegexForToken(token, config) {
  18121. var a, strict = config._strict;
  18122. switch (token) {
  18123. case 'Q':
  18124. return parseTokenOneDigit;
  18125. case 'DDDD':
  18126. return parseTokenThreeDigits;
  18127. case 'YYYY':
  18128. case 'GGGG':
  18129. case 'gggg':
  18130. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  18131. case 'Y':
  18132. case 'G':
  18133. case 'g':
  18134. return parseTokenSignedNumber;
  18135. case 'YYYYYY':
  18136. case 'YYYYY':
  18137. case 'GGGGG':
  18138. case 'ggggg':
  18139. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  18140. case 'S':
  18141. if (strict) { return parseTokenOneDigit; }
  18142. /* falls through */
  18143. case 'SS':
  18144. if (strict) { return parseTokenTwoDigits; }
  18145. /* falls through */
  18146. case 'SSS':
  18147. if (strict) { return parseTokenThreeDigits; }
  18148. /* falls through */
  18149. case 'DDD':
  18150. return parseTokenOneToThreeDigits;
  18151. case 'MMM':
  18152. case 'MMMM':
  18153. case 'dd':
  18154. case 'ddd':
  18155. case 'dddd':
  18156. return parseTokenWord;
  18157. case 'a':
  18158. case 'A':
  18159. return getLangDefinition(config._l)._meridiemParse;
  18160. case 'X':
  18161. return parseTokenTimestampMs;
  18162. case 'Z':
  18163. case 'ZZ':
  18164. return parseTokenTimezone;
  18165. case 'T':
  18166. return parseTokenT;
  18167. case 'SSSS':
  18168. return parseTokenDigits;
  18169. case 'MM':
  18170. case 'DD':
  18171. case 'YY':
  18172. case 'GG':
  18173. case 'gg':
  18174. case 'HH':
  18175. case 'hh':
  18176. case 'mm':
  18177. case 'ss':
  18178. case 'ww':
  18179. case 'WW':
  18180. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  18181. case 'M':
  18182. case 'D':
  18183. case 'd':
  18184. case 'H':
  18185. case 'h':
  18186. case 'm':
  18187. case 's':
  18188. case 'w':
  18189. case 'W':
  18190. case 'e':
  18191. case 'E':
  18192. return parseTokenOneOrTwoDigits;
  18193. case 'Do':
  18194. return parseTokenOrdinal;
  18195. default :
  18196. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  18197. return a;
  18198. }
  18199. }
  18200. function timezoneMinutesFromString(string) {
  18201. string = string || "";
  18202. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  18203. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  18204. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  18205. minutes = +(parts[1] * 60) + toInt(parts[2]);
  18206. return parts[0] === '+' ? -minutes : minutes;
  18207. }
  18208. // function to convert string input to date
  18209. function addTimeToArrayFromToken(token, input, config) {
  18210. var a, datePartArray = config._a;
  18211. switch (token) {
  18212. // QUARTER
  18213. case 'Q':
  18214. if (input != null) {
  18215. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  18216. }
  18217. break;
  18218. // MONTH
  18219. case 'M' : // fall through to MM
  18220. case 'MM' :
  18221. if (input != null) {
  18222. datePartArray[MONTH] = toInt(input) - 1;
  18223. }
  18224. break;
  18225. case 'MMM' : // fall through to MMMM
  18226. case 'MMMM' :
  18227. a = getLangDefinition(config._l).monthsParse(input);
  18228. // if we didn't find a month name, mark the date as invalid.
  18229. if (a != null) {
  18230. datePartArray[MONTH] = a;
  18231. } else {
  18232. config._pf.invalidMonth = input;
  18233. }
  18234. break;
  18235. // DAY OF MONTH
  18236. case 'D' : // fall through to DD
  18237. case 'DD' :
  18238. if (input != null) {
  18239. datePartArray[DATE] = toInt(input);
  18240. }
  18241. break;
  18242. case 'Do' :
  18243. if (input != null) {
  18244. datePartArray[DATE] = toInt(parseInt(input, 10));
  18245. }
  18246. break;
  18247. // DAY OF YEAR
  18248. case 'DDD' : // fall through to DDDD
  18249. case 'DDDD' :
  18250. if (input != null) {
  18251. config._dayOfYear = toInt(input);
  18252. }
  18253. break;
  18254. // YEAR
  18255. case 'YY' :
  18256. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  18257. break;
  18258. case 'YYYY' :
  18259. case 'YYYYY' :
  18260. case 'YYYYYY' :
  18261. datePartArray[YEAR] = toInt(input);
  18262. break;
  18263. // AM / PM
  18264. case 'a' : // fall through to A
  18265. case 'A' :
  18266. config._isPm = getLangDefinition(config._l).isPM(input);
  18267. break;
  18268. // 24 HOUR
  18269. case 'H' : // fall through to hh
  18270. case 'HH' : // fall through to hh
  18271. case 'h' : // fall through to hh
  18272. case 'hh' :
  18273. datePartArray[HOUR] = toInt(input);
  18274. break;
  18275. // MINUTE
  18276. case 'm' : // fall through to mm
  18277. case 'mm' :
  18278. datePartArray[MINUTE] = toInt(input);
  18279. break;
  18280. // SECOND
  18281. case 's' : // fall through to ss
  18282. case 'ss' :
  18283. datePartArray[SECOND] = toInt(input);
  18284. break;
  18285. // MILLISECOND
  18286. case 'S' :
  18287. case 'SS' :
  18288. case 'SSS' :
  18289. case 'SSSS' :
  18290. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  18291. break;
  18292. // UNIX TIMESTAMP WITH MS
  18293. case 'X':
  18294. config._d = new Date(parseFloat(input) * 1000);
  18295. break;
  18296. // TIMEZONE
  18297. case 'Z' : // fall through to ZZ
  18298. case 'ZZ' :
  18299. config._useUTC = true;
  18300. config._tzm = timezoneMinutesFromString(input);
  18301. break;
  18302. case 'w':
  18303. case 'ww':
  18304. case 'W':
  18305. case 'WW':
  18306. case 'd':
  18307. case 'dd':
  18308. case 'ddd':
  18309. case 'dddd':
  18310. case 'e':
  18311. case 'E':
  18312. token = token.substr(0, 1);
  18313. /* falls through */
  18314. case 'gg':
  18315. case 'gggg':
  18316. case 'GG':
  18317. case 'GGGG':
  18318. case 'GGGGG':
  18319. token = token.substr(0, 2);
  18320. if (input) {
  18321. config._w = config._w || {};
  18322. config._w[token] = input;
  18323. }
  18324. break;
  18325. }
  18326. }
  18327. // convert an array to a date.
  18328. // the array should mirror the parameters below
  18329. // note: all values past the year are optional and will default to the lowest possible value.
  18330. // [year, month, day , hour, minute, second, millisecond]
  18331. function dateFromConfig(config) {
  18332. var i, date, input = [], currentDate,
  18333. yearToUse, fixYear, w, temp, lang, weekday, week;
  18334. if (config._d) {
  18335. return;
  18336. }
  18337. currentDate = currentDateArray(config);
  18338. //compute day of the year from weeks and weekdays
  18339. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  18340. fixYear = function (val) {
  18341. var intVal = parseInt(val, 10);
  18342. return val ?
  18343. (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
  18344. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  18345. };
  18346. w = config._w;
  18347. if (w.GG != null || w.W != null || w.E != null) {
  18348. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  18349. }
  18350. else {
  18351. lang = getLangDefinition(config._l);
  18352. weekday = w.d != null ? parseWeekday(w.d, lang) :
  18353. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  18354. week = parseInt(w.w, 10) || 1;
  18355. //if we're parsing 'd', then the low day numbers may be next week
  18356. if (w.d != null && weekday < lang._week.dow) {
  18357. week++;
  18358. }
  18359. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  18360. }
  18361. config._a[YEAR] = temp.year;
  18362. config._dayOfYear = temp.dayOfYear;
  18363. }
  18364. //if the day of the year is set, figure out what it is
  18365. if (config._dayOfYear) {
  18366. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  18367. if (config._dayOfYear > daysInYear(yearToUse)) {
  18368. config._pf._overflowDayOfYear = true;
  18369. }
  18370. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  18371. config._a[MONTH] = date.getUTCMonth();
  18372. config._a[DATE] = date.getUTCDate();
  18373. }
  18374. // Default to current date.
  18375. // * if no year, month, day of month are given, default to today
  18376. // * if day of month is given, default month and year
  18377. // * if month is given, default only year
  18378. // * if year is given, don't default anything
  18379. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  18380. config._a[i] = input[i] = currentDate[i];
  18381. }
  18382. // Zero out whatever was not defaulted, including time
  18383. for (; i < 7; i++) {
  18384. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  18385. }
  18386. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  18387. input[HOUR] += toInt((config._tzm || 0) / 60);
  18388. input[MINUTE] += toInt((config._tzm || 0) % 60);
  18389. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  18390. }
  18391. function dateFromObject(config) {
  18392. var normalizedInput;
  18393. if (config._d) {
  18394. return;
  18395. }
  18396. normalizedInput = normalizeObjectUnits(config._i);
  18397. config._a = [
  18398. normalizedInput.year,
  18399. normalizedInput.month,
  18400. normalizedInput.day,
  18401. normalizedInput.hour,
  18402. normalizedInput.minute,
  18403. normalizedInput.second,
  18404. normalizedInput.millisecond
  18405. ];
  18406. dateFromConfig(config);
  18407. }
  18408. function currentDateArray(config) {
  18409. var now = new Date();
  18410. if (config._useUTC) {
  18411. return [
  18412. now.getUTCFullYear(),
  18413. now.getUTCMonth(),
  18414. now.getUTCDate()
  18415. ];
  18416. } else {
  18417. return [now.getFullYear(), now.getMonth(), now.getDate()];
  18418. }
  18419. }
  18420. // date from string and format string
  18421. function makeDateFromStringAndFormat(config) {
  18422. config._a = [];
  18423. config._pf.empty = true;
  18424. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  18425. var lang = getLangDefinition(config._l),
  18426. string = '' + config._i,
  18427. i, parsedInput, tokens, token, skipped,
  18428. stringLength = string.length,
  18429. totalParsedInputLength = 0;
  18430. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  18431. for (i = 0; i < tokens.length; i++) {
  18432. token = tokens[i];
  18433. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  18434. if (parsedInput) {
  18435. skipped = string.substr(0, string.indexOf(parsedInput));
  18436. if (skipped.length > 0) {
  18437. config._pf.unusedInput.push(skipped);
  18438. }
  18439. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  18440. totalParsedInputLength += parsedInput.length;
  18441. }
  18442. // don't parse if it's not a known token
  18443. if (formatTokenFunctions[token]) {
  18444. if (parsedInput) {
  18445. config._pf.empty = false;
  18446. }
  18447. else {
  18448. config._pf.unusedTokens.push(token);
  18449. }
  18450. addTimeToArrayFromToken(token, parsedInput, config);
  18451. }
  18452. else if (config._strict && !parsedInput) {
  18453. config._pf.unusedTokens.push(token);
  18454. }
  18455. }
  18456. // add remaining unparsed input length to the string
  18457. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  18458. if (string.length > 0) {
  18459. config._pf.unusedInput.push(string);
  18460. }
  18461. // handle am pm
  18462. if (config._isPm && config._a[HOUR] < 12) {
  18463. config._a[HOUR] += 12;
  18464. }
  18465. // if is 12 am, change hours to 0
  18466. if (config._isPm === false && config._a[HOUR] === 12) {
  18467. config._a[HOUR] = 0;
  18468. }
  18469. dateFromConfig(config);
  18470. checkOverflow(config);
  18471. }
  18472. function unescapeFormat(s) {
  18473. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  18474. return p1 || p2 || p3 || p4;
  18475. });
  18476. }
  18477. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  18478. function regexpEscape(s) {
  18479. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  18480. }
  18481. // date from string and array of format strings
  18482. function makeDateFromStringAndArray(config) {
  18483. var tempConfig,
  18484. bestMoment,
  18485. scoreToBeat,
  18486. i,
  18487. currentScore;
  18488. if (config._f.length === 0) {
  18489. config._pf.invalidFormat = true;
  18490. config._d = new Date(NaN);
  18491. return;
  18492. }
  18493. for (i = 0; i < config._f.length; i++) {
  18494. currentScore = 0;
  18495. tempConfig = extend({}, config);
  18496. tempConfig._pf = defaultParsingFlags();
  18497. tempConfig._f = config._f[i];
  18498. makeDateFromStringAndFormat(tempConfig);
  18499. if (!isValid(tempConfig)) {
  18500. continue;
  18501. }
  18502. // if there is any input that was not parsed add a penalty for that format
  18503. currentScore += tempConfig._pf.charsLeftOver;
  18504. //or tokens
  18505. currentScore += tempConfig._pf.unusedTokens.length * 10;
  18506. tempConfig._pf.score = currentScore;
  18507. if (scoreToBeat == null || currentScore < scoreToBeat) {
  18508. scoreToBeat = currentScore;
  18509. bestMoment = tempConfig;
  18510. }
  18511. }
  18512. extend(config, bestMoment || tempConfig);
  18513. }
  18514. // date from iso format
  18515. function makeDateFromString(config) {
  18516. var i, l,
  18517. string = config._i,
  18518. match = isoRegex.exec(string);
  18519. if (match) {
  18520. config._pf.iso = true;
  18521. for (i = 0, l = isoDates.length; i < l; i++) {
  18522. if (isoDates[i][1].exec(string)) {
  18523. // match[5] should be "T" or undefined
  18524. config._f = isoDates[i][0] + (match[6] || " ");
  18525. break;
  18526. }
  18527. }
  18528. for (i = 0, l = isoTimes.length; i < l; i++) {
  18529. if (isoTimes[i][1].exec(string)) {
  18530. config._f += isoTimes[i][0];
  18531. break;
  18532. }
  18533. }
  18534. if (string.match(parseTokenTimezone)) {
  18535. config._f += "Z";
  18536. }
  18537. makeDateFromStringAndFormat(config);
  18538. }
  18539. else {
  18540. moment.createFromInputFallback(config);
  18541. }
  18542. }
  18543. function makeDateFromInput(config) {
  18544. var input = config._i,
  18545. matched = aspNetJsonRegex.exec(input);
  18546. if (input === undefined) {
  18547. config._d = new Date();
  18548. } else if (matched) {
  18549. config._d = new Date(+matched[1]);
  18550. } else if (typeof input === 'string') {
  18551. makeDateFromString(config);
  18552. } else if (isArray(input)) {
  18553. config._a = input.slice(0);
  18554. dateFromConfig(config);
  18555. } else if (isDate(input)) {
  18556. config._d = new Date(+input);
  18557. } else if (typeof(input) === 'object') {
  18558. dateFromObject(config);
  18559. } else if (typeof(input) === 'number') {
  18560. // from milliseconds
  18561. config._d = new Date(input);
  18562. } else {
  18563. moment.createFromInputFallback(config);
  18564. }
  18565. }
  18566. function makeDate(y, m, d, h, M, s, ms) {
  18567. //can't just apply() to create a date:
  18568. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  18569. var date = new Date(y, m, d, h, M, s, ms);
  18570. //the date constructor doesn't accept years < 1970
  18571. if (y < 1970) {
  18572. date.setFullYear(y);
  18573. }
  18574. return date;
  18575. }
  18576. function makeUTCDate(y) {
  18577. var date = new Date(Date.UTC.apply(null, arguments));
  18578. if (y < 1970) {
  18579. date.setUTCFullYear(y);
  18580. }
  18581. return date;
  18582. }
  18583. function parseWeekday(input, language) {
  18584. if (typeof input === 'string') {
  18585. if (!isNaN(input)) {
  18586. input = parseInt(input, 10);
  18587. }
  18588. else {
  18589. input = language.weekdaysParse(input);
  18590. if (typeof input !== 'number') {
  18591. return null;
  18592. }
  18593. }
  18594. }
  18595. return input;
  18596. }
  18597. /************************************
  18598. Relative Time
  18599. ************************************/
  18600. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  18601. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  18602. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  18603. }
  18604. function relativeTime(milliseconds, withoutSuffix, lang) {
  18605. var seconds = round(Math.abs(milliseconds) / 1000),
  18606. minutes = round(seconds / 60),
  18607. hours = round(minutes / 60),
  18608. days = round(hours / 24),
  18609. years = round(days / 365),
  18610. args = seconds < 45 && ['s', seconds] ||
  18611. minutes === 1 && ['m'] ||
  18612. minutes < 45 && ['mm', minutes] ||
  18613. hours === 1 && ['h'] ||
  18614. hours < 22 && ['hh', hours] ||
  18615. days === 1 && ['d'] ||
  18616. days <= 25 && ['dd', days] ||
  18617. days <= 45 && ['M'] ||
  18618. days < 345 && ['MM', round(days / 30)] ||
  18619. years === 1 && ['y'] || ['yy', years];
  18620. args[2] = withoutSuffix;
  18621. args[3] = milliseconds > 0;
  18622. args[4] = lang;
  18623. return substituteTimeAgo.apply({}, args);
  18624. }
  18625. /************************************
  18626. Week of Year
  18627. ************************************/
  18628. // firstDayOfWeek 0 = sun, 6 = sat
  18629. // the day of the week that starts the week
  18630. // (usually sunday or monday)
  18631. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  18632. // the first week is the week that contains the first
  18633. // of this day of the week
  18634. // (eg. ISO weeks use thursday (4))
  18635. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  18636. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  18637. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  18638. adjustedMoment;
  18639. if (daysToDayOfWeek > end) {
  18640. daysToDayOfWeek -= 7;
  18641. }
  18642. if (daysToDayOfWeek < end - 7) {
  18643. daysToDayOfWeek += 7;
  18644. }
  18645. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  18646. return {
  18647. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  18648. year: adjustedMoment.year()
  18649. };
  18650. }
  18651. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  18652. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  18653. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  18654. weekday = weekday != null ? weekday : firstDayOfWeek;
  18655. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  18656. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  18657. return {
  18658. year: dayOfYear > 0 ? year : year - 1,
  18659. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  18660. };
  18661. }
  18662. /************************************
  18663. Top Level Functions
  18664. ************************************/
  18665. function makeMoment(config) {
  18666. var input = config._i,
  18667. format = config._f;
  18668. if (input === null || (format === undefined && input === '')) {
  18669. return moment.invalid({nullInput: true});
  18670. }
  18671. if (typeof input === 'string') {
  18672. config._i = input = getLangDefinition().preparse(input);
  18673. }
  18674. if (moment.isMoment(input)) {
  18675. config = cloneMoment(input);
  18676. config._d = new Date(+input._d);
  18677. } else if (format) {
  18678. if (isArray(format)) {
  18679. makeDateFromStringAndArray(config);
  18680. } else {
  18681. makeDateFromStringAndFormat(config);
  18682. }
  18683. } else {
  18684. makeDateFromInput(config);
  18685. }
  18686. return new Moment(config);
  18687. }
  18688. moment = function (input, format, lang, strict) {
  18689. var c;
  18690. if (typeof(lang) === "boolean") {
  18691. strict = lang;
  18692. lang = undefined;
  18693. }
  18694. // object construction must be done this way.
  18695. // https://github.com/moment/moment/issues/1423
  18696. c = {};
  18697. c._isAMomentObject = true;
  18698. c._i = input;
  18699. c._f = format;
  18700. c._l = lang;
  18701. c._strict = strict;
  18702. c._isUTC = false;
  18703. c._pf = defaultParsingFlags();
  18704. return makeMoment(c);
  18705. };
  18706. moment.suppressDeprecationWarnings = false;
  18707. moment.createFromInputFallback = deprecate(
  18708. "moment construction falls back to js Date. This is " +
  18709. "discouraged and will be removed in upcoming major " +
  18710. "release. Please refer to " +
  18711. "https://github.com/moment/moment/issues/1407 for more info.",
  18712. function (config) {
  18713. config._d = new Date(config._i);
  18714. });
  18715. // creating with utc
  18716. moment.utc = function (input, format, lang, strict) {
  18717. var c;
  18718. if (typeof(lang) === "boolean") {
  18719. strict = lang;
  18720. lang = undefined;
  18721. }
  18722. // object construction must be done this way.
  18723. // https://github.com/moment/moment/issues/1423
  18724. c = {};
  18725. c._isAMomentObject = true;
  18726. c._useUTC = true;
  18727. c._isUTC = true;
  18728. c._l = lang;
  18729. c._i = input;
  18730. c._f = format;
  18731. c._strict = strict;
  18732. c._pf = defaultParsingFlags();
  18733. return makeMoment(c).utc();
  18734. };
  18735. // creating with unix timestamp (in seconds)
  18736. moment.unix = function (input) {
  18737. return moment(input * 1000);
  18738. };
  18739. // duration
  18740. moment.duration = function (input, key) {
  18741. var duration = input,
  18742. // matching against regexp is expensive, do it on demand
  18743. match = null,
  18744. sign,
  18745. ret,
  18746. parseIso;
  18747. if (moment.isDuration(input)) {
  18748. duration = {
  18749. ms: input._milliseconds,
  18750. d: input._days,
  18751. M: input._months
  18752. };
  18753. } else if (typeof input === 'number') {
  18754. duration = {};
  18755. if (key) {
  18756. duration[key] = input;
  18757. } else {
  18758. duration.milliseconds = input;
  18759. }
  18760. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  18761. sign = (match[1] === "-") ? -1 : 1;
  18762. duration = {
  18763. y: 0,
  18764. d: toInt(match[DATE]) * sign,
  18765. h: toInt(match[HOUR]) * sign,
  18766. m: toInt(match[MINUTE]) * sign,
  18767. s: toInt(match[SECOND]) * sign,
  18768. ms: toInt(match[MILLISECOND]) * sign
  18769. };
  18770. } else if (!!(match = isoDurationRegex.exec(input))) {
  18771. sign = (match[1] === "-") ? -1 : 1;
  18772. parseIso = function (inp) {
  18773. // We'd normally use ~~inp for this, but unfortunately it also
  18774. // converts floats to ints.
  18775. // inp may be undefined, so careful calling replace on it.
  18776. var res = inp && parseFloat(inp.replace(',', '.'));
  18777. // apply sign while we're at it
  18778. return (isNaN(res) ? 0 : res) * sign;
  18779. };
  18780. duration = {
  18781. y: parseIso(match[2]),
  18782. M: parseIso(match[3]),
  18783. d: parseIso(match[4]),
  18784. h: parseIso(match[5]),
  18785. m: parseIso(match[6]),
  18786. s: parseIso(match[7]),
  18787. w: parseIso(match[8])
  18788. };
  18789. }
  18790. ret = new Duration(duration);
  18791. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  18792. ret._lang = input._lang;
  18793. }
  18794. return ret;
  18795. };
  18796. // version number
  18797. moment.version = VERSION;
  18798. // default format
  18799. moment.defaultFormat = isoFormat;
  18800. // Plugins that add properties should also add the key here (null value),
  18801. // so we can properly clone ourselves.
  18802. moment.momentProperties = momentProperties;
  18803. // This function will be called whenever a moment is mutated.
  18804. // It is intended to keep the offset in sync with the timezone.
  18805. moment.updateOffset = function () {};
  18806. // This function will load languages and then set the global language. If
  18807. // no arguments are passed in, it will simply return the current global
  18808. // language key.
  18809. moment.lang = function (key, values) {
  18810. var r;
  18811. if (!key) {
  18812. return moment.fn._lang._abbr;
  18813. }
  18814. if (values) {
  18815. loadLang(normalizeLanguage(key), values);
  18816. } else if (values === null) {
  18817. unloadLang(key);
  18818. key = 'en';
  18819. } else if (!languages[key]) {
  18820. getLangDefinition(key);
  18821. }
  18822. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  18823. return r._abbr;
  18824. };
  18825. // returns language data
  18826. moment.langData = function (key) {
  18827. if (key && key._lang && key._lang._abbr) {
  18828. key = key._lang._abbr;
  18829. }
  18830. return getLangDefinition(key);
  18831. };
  18832. // compare moment object
  18833. moment.isMoment = function (obj) {
  18834. return obj instanceof Moment ||
  18835. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  18836. };
  18837. // for typechecking Duration objects
  18838. moment.isDuration = function (obj) {
  18839. return obj instanceof Duration;
  18840. };
  18841. for (i = lists.length - 1; i >= 0; --i) {
  18842. makeList(lists[i]);
  18843. }
  18844. moment.normalizeUnits = function (units) {
  18845. return normalizeUnits(units);
  18846. };
  18847. moment.invalid = function (flags) {
  18848. var m = moment.utc(NaN);
  18849. if (flags != null) {
  18850. extend(m._pf, flags);
  18851. }
  18852. else {
  18853. m._pf.userInvalidated = true;
  18854. }
  18855. return m;
  18856. };
  18857. moment.parseZone = function () {
  18858. return moment.apply(null, arguments).parseZone();
  18859. };
  18860. moment.parseTwoDigitYear = function (input) {
  18861. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  18862. };
  18863. /************************************
  18864. Moment Prototype
  18865. ************************************/
  18866. extend(moment.fn = Moment.prototype, {
  18867. clone : function () {
  18868. return moment(this);
  18869. },
  18870. valueOf : function () {
  18871. return +this._d + ((this._offset || 0) * 60000);
  18872. },
  18873. unix : function () {
  18874. return Math.floor(+this / 1000);
  18875. },
  18876. toString : function () {
  18877. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  18878. },
  18879. toDate : function () {
  18880. return this._offset ? new Date(+this) : this._d;
  18881. },
  18882. toISOString : function () {
  18883. var m = moment(this).utc();
  18884. if (0 < m.year() && m.year() <= 9999) {
  18885. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18886. } else {
  18887. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18888. }
  18889. },
  18890. toArray : function () {
  18891. var m = this;
  18892. return [
  18893. m.year(),
  18894. m.month(),
  18895. m.date(),
  18896. m.hours(),
  18897. m.minutes(),
  18898. m.seconds(),
  18899. m.milliseconds()
  18900. ];
  18901. },
  18902. isValid : function () {
  18903. return isValid(this);
  18904. },
  18905. isDSTShifted : function () {
  18906. if (this._a) {
  18907. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  18908. }
  18909. return false;
  18910. },
  18911. parsingFlags : function () {
  18912. return extend({}, this._pf);
  18913. },
  18914. invalidAt: function () {
  18915. return this._pf.overflow;
  18916. },
  18917. utc : function () {
  18918. return this.zone(0);
  18919. },
  18920. local : function () {
  18921. this.zone(0);
  18922. this._isUTC = false;
  18923. return this;
  18924. },
  18925. format : function (inputString) {
  18926. var output = formatMoment(this, inputString || moment.defaultFormat);
  18927. return this.lang().postformat(output);
  18928. },
  18929. add : function (input, val) {
  18930. var dur;
  18931. // switch args to support add('s', 1) and add(1, 's')
  18932. if (typeof input === 'string') {
  18933. dur = moment.duration(+val, input);
  18934. } else {
  18935. dur = moment.duration(input, val);
  18936. }
  18937. addOrSubtractDurationFromMoment(this, dur, 1);
  18938. return this;
  18939. },
  18940. subtract : function (input, val) {
  18941. var dur;
  18942. // switch args to support subtract('s', 1) and subtract(1, 's')
  18943. if (typeof input === 'string') {
  18944. dur = moment.duration(+val, input);
  18945. } else {
  18946. dur = moment.duration(input, val);
  18947. }
  18948. addOrSubtractDurationFromMoment(this, dur, -1);
  18949. return this;
  18950. },
  18951. diff : function (input, units, asFloat) {
  18952. var that = makeAs(input, this),
  18953. zoneDiff = (this.zone() - that.zone()) * 6e4,
  18954. diff, output;
  18955. units = normalizeUnits(units);
  18956. if (units === 'year' || units === 'month') {
  18957. // average number of days in the months in the given dates
  18958. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  18959. // difference in months
  18960. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  18961. // adjust by taking difference in days, average number of days
  18962. // and dst in the given months.
  18963. output += ((this - moment(this).startOf('month')) -
  18964. (that - moment(that).startOf('month'))) / diff;
  18965. // same as above but with zones, to negate all dst
  18966. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  18967. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  18968. if (units === 'year') {
  18969. output = output / 12;
  18970. }
  18971. } else {
  18972. diff = (this - that);
  18973. output = units === 'second' ? diff / 1e3 : // 1000
  18974. units === 'minute' ? diff / 6e4 : // 1000 * 60
  18975. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  18976. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  18977. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  18978. diff;
  18979. }
  18980. return asFloat ? output : absRound(output);
  18981. },
  18982. from : function (time, withoutSuffix) {
  18983. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  18984. },
  18985. fromNow : function (withoutSuffix) {
  18986. return this.from(moment(), withoutSuffix);
  18987. },
  18988. calendar : function () {
  18989. // We want to compare the start of today, vs this.
  18990. // Getting start-of-today depends on whether we're zone'd or not.
  18991. var sod = makeAs(moment(), this).startOf('day'),
  18992. diff = this.diff(sod, 'days', true),
  18993. format = diff < -6 ? 'sameElse' :
  18994. diff < -1 ? 'lastWeek' :
  18995. diff < 0 ? 'lastDay' :
  18996. diff < 1 ? 'sameDay' :
  18997. diff < 2 ? 'nextDay' :
  18998. diff < 7 ? 'nextWeek' : 'sameElse';
  18999. return this.format(this.lang().calendar(format, this));
  19000. },
  19001. isLeapYear : function () {
  19002. return isLeapYear(this.year());
  19003. },
  19004. isDST : function () {
  19005. return (this.zone() < this.clone().month(0).zone() ||
  19006. this.zone() < this.clone().month(5).zone());
  19007. },
  19008. day : function (input) {
  19009. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  19010. if (input != null) {
  19011. input = parseWeekday(input, this.lang());
  19012. return this.add({ d : input - day });
  19013. } else {
  19014. return day;
  19015. }
  19016. },
  19017. month : makeAccessor('Month', true),
  19018. startOf: function (units) {
  19019. units = normalizeUnits(units);
  19020. // the following switch intentionally omits break keywords
  19021. // to utilize falling through the cases.
  19022. switch (units) {
  19023. case 'year':
  19024. this.month(0);
  19025. /* falls through */
  19026. case 'quarter':
  19027. case 'month':
  19028. this.date(1);
  19029. /* falls through */
  19030. case 'week':
  19031. case 'isoWeek':
  19032. case 'day':
  19033. this.hours(0);
  19034. /* falls through */
  19035. case 'hour':
  19036. this.minutes(0);
  19037. /* falls through */
  19038. case 'minute':
  19039. this.seconds(0);
  19040. /* falls through */
  19041. case 'second':
  19042. this.milliseconds(0);
  19043. /* falls through */
  19044. }
  19045. // weeks are a special case
  19046. if (units === 'week') {
  19047. this.weekday(0);
  19048. } else if (units === 'isoWeek') {
  19049. this.isoWeekday(1);
  19050. }
  19051. // quarters are also special
  19052. if (units === 'quarter') {
  19053. this.month(Math.floor(this.month() / 3) * 3);
  19054. }
  19055. return this;
  19056. },
  19057. endOf: function (units) {
  19058. units = normalizeUnits(units);
  19059. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  19060. },
  19061. isAfter: function (input, units) {
  19062. units = typeof units !== 'undefined' ? units : 'millisecond';
  19063. return +this.clone().startOf(units) > +moment(input).startOf(units);
  19064. },
  19065. isBefore: function (input, units) {
  19066. units = typeof units !== 'undefined' ? units : 'millisecond';
  19067. return +this.clone().startOf(units) < +moment(input).startOf(units);
  19068. },
  19069. isSame: function (input, units) {
  19070. units = units || 'ms';
  19071. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  19072. },
  19073. min: function (other) {
  19074. other = moment.apply(null, arguments);
  19075. return other < this ? this : other;
  19076. },
  19077. max: function (other) {
  19078. other = moment.apply(null, arguments);
  19079. return other > this ? this : other;
  19080. },
  19081. // keepTime = true means only change the timezone, without affecting
  19082. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  19083. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  19084. // adjust the time as needed, to be valid.
  19085. //
  19086. // Keeping the time actually adds/subtracts (one hour)
  19087. // from the actual represented time. That is why we call updateOffset
  19088. // a second time. In case it wants us to change the offset again
  19089. // _changeInProgress == true case, then we have to adjust, because
  19090. // there is no such time in the given timezone.
  19091. zone : function (input, keepTime) {
  19092. var offset = this._offset || 0;
  19093. if (input != null) {
  19094. if (typeof input === "string") {
  19095. input = timezoneMinutesFromString(input);
  19096. }
  19097. if (Math.abs(input) < 16) {
  19098. input = input * 60;
  19099. }
  19100. this._offset = input;
  19101. this._isUTC = true;
  19102. if (offset !== input) {
  19103. if (!keepTime || this._changeInProgress) {
  19104. addOrSubtractDurationFromMoment(this,
  19105. moment.duration(offset - input, 'm'), 1, false);
  19106. } else if (!this._changeInProgress) {
  19107. this._changeInProgress = true;
  19108. moment.updateOffset(this, true);
  19109. this._changeInProgress = null;
  19110. }
  19111. }
  19112. } else {
  19113. return this._isUTC ? offset : this._d.getTimezoneOffset();
  19114. }
  19115. return this;
  19116. },
  19117. zoneAbbr : function () {
  19118. return this._isUTC ? "UTC" : "";
  19119. },
  19120. zoneName : function () {
  19121. return this._isUTC ? "Coordinated Universal Time" : "";
  19122. },
  19123. parseZone : function () {
  19124. if (this._tzm) {
  19125. this.zone(this._tzm);
  19126. } else if (typeof this._i === 'string') {
  19127. this.zone(this._i);
  19128. }
  19129. return this;
  19130. },
  19131. hasAlignedHourOffset : function (input) {
  19132. if (!input) {
  19133. input = 0;
  19134. }
  19135. else {
  19136. input = moment(input).zone();
  19137. }
  19138. return (this.zone() - input) % 60 === 0;
  19139. },
  19140. daysInMonth : function () {
  19141. return daysInMonth(this.year(), this.month());
  19142. },
  19143. dayOfYear : function (input) {
  19144. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  19145. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  19146. },
  19147. quarter : function (input) {
  19148. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  19149. },
  19150. weekYear : function (input) {
  19151. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  19152. return input == null ? year : this.add("y", (input - year));
  19153. },
  19154. isoWeekYear : function (input) {
  19155. var year = weekOfYear(this, 1, 4).year;
  19156. return input == null ? year : this.add("y", (input - year));
  19157. },
  19158. week : function (input) {
  19159. var week = this.lang().week(this);
  19160. return input == null ? week : this.add("d", (input - week) * 7);
  19161. },
  19162. isoWeek : function (input) {
  19163. var week = weekOfYear(this, 1, 4).week;
  19164. return input == null ? week : this.add("d", (input - week) * 7);
  19165. },
  19166. weekday : function (input) {
  19167. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  19168. return input == null ? weekday : this.add("d", input - weekday);
  19169. },
  19170. isoWeekday : function (input) {
  19171. // behaves the same as moment#day except
  19172. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  19173. // as a setter, sunday should belong to the previous week.
  19174. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  19175. },
  19176. isoWeeksInYear : function () {
  19177. return weeksInYear(this.year(), 1, 4);
  19178. },
  19179. weeksInYear : function () {
  19180. var weekInfo = this._lang._week;
  19181. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  19182. },
  19183. get : function (units) {
  19184. units = normalizeUnits(units);
  19185. return this[units]();
  19186. },
  19187. set : function (units, value) {
  19188. units = normalizeUnits(units);
  19189. if (typeof this[units] === 'function') {
  19190. this[units](value);
  19191. }
  19192. return this;
  19193. },
  19194. // If passed a language key, it will set the language for this
  19195. // instance. Otherwise, it will return the language configuration
  19196. // variables for this instance.
  19197. lang : function (key) {
  19198. if (key === undefined) {
  19199. return this._lang;
  19200. } else {
  19201. this._lang = getLangDefinition(key);
  19202. return this;
  19203. }
  19204. }
  19205. });
  19206. function rawMonthSetter(mom, value) {
  19207. var dayOfMonth;
  19208. // TODO: Move this out of here!
  19209. if (typeof value === 'string') {
  19210. value = mom.lang().monthsParse(value);
  19211. // TODO: Another silent failure?
  19212. if (typeof value !== 'number') {
  19213. return mom;
  19214. }
  19215. }
  19216. dayOfMonth = Math.min(mom.date(),
  19217. daysInMonth(mom.year(), value));
  19218. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  19219. return mom;
  19220. }
  19221. function rawGetter(mom, unit) {
  19222. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  19223. }
  19224. function rawSetter(mom, unit, value) {
  19225. if (unit === 'Month') {
  19226. return rawMonthSetter(mom, value);
  19227. } else {
  19228. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  19229. }
  19230. }
  19231. function makeAccessor(unit, keepTime) {
  19232. return function (value) {
  19233. if (value != null) {
  19234. rawSetter(this, unit, value);
  19235. moment.updateOffset(this, keepTime);
  19236. return this;
  19237. } else {
  19238. return rawGetter(this, unit);
  19239. }
  19240. };
  19241. }
  19242. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  19243. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  19244. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  19245. // Setting the hour should keep the time, because the user explicitly
  19246. // specified which hour he wants. So trying to maintain the same hour (in
  19247. // a new timezone) makes sense. Adding/subtracting hours does not follow
  19248. // this rule.
  19249. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  19250. // moment.fn.month is defined separately
  19251. moment.fn.date = makeAccessor('Date', true);
  19252. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  19253. moment.fn.year = makeAccessor('FullYear', true);
  19254. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  19255. // add plural methods
  19256. moment.fn.days = moment.fn.day;
  19257. moment.fn.months = moment.fn.month;
  19258. moment.fn.weeks = moment.fn.week;
  19259. moment.fn.isoWeeks = moment.fn.isoWeek;
  19260. moment.fn.quarters = moment.fn.quarter;
  19261. // add aliased format methods
  19262. moment.fn.toJSON = moment.fn.toISOString;
  19263. /************************************
  19264. Duration Prototype
  19265. ************************************/
  19266. extend(moment.duration.fn = Duration.prototype, {
  19267. _bubble : function () {
  19268. var milliseconds = this._milliseconds,
  19269. days = this._days,
  19270. months = this._months,
  19271. data = this._data,
  19272. seconds, minutes, hours, years;
  19273. // The following code bubbles up values, see the tests for
  19274. // examples of what that means.
  19275. data.milliseconds = milliseconds % 1000;
  19276. seconds = absRound(milliseconds / 1000);
  19277. data.seconds = seconds % 60;
  19278. minutes = absRound(seconds / 60);
  19279. data.minutes = minutes % 60;
  19280. hours = absRound(minutes / 60);
  19281. data.hours = hours % 24;
  19282. days += absRound(hours / 24);
  19283. data.days = days % 30;
  19284. months += absRound(days / 30);
  19285. data.months = months % 12;
  19286. years = absRound(months / 12);
  19287. data.years = years;
  19288. },
  19289. weeks : function () {
  19290. return absRound(this.days() / 7);
  19291. },
  19292. valueOf : function () {
  19293. return this._milliseconds +
  19294. this._days * 864e5 +
  19295. (this._months % 12) * 2592e6 +
  19296. toInt(this._months / 12) * 31536e6;
  19297. },
  19298. humanize : function (withSuffix) {
  19299. var difference = +this,
  19300. output = relativeTime(difference, !withSuffix, this.lang());
  19301. if (withSuffix) {
  19302. output = this.lang().pastFuture(difference, output);
  19303. }
  19304. return this.lang().postformat(output);
  19305. },
  19306. add : function (input, val) {
  19307. // supports only 2.0-style add(1, 's') or add(moment)
  19308. var dur = moment.duration(input, val);
  19309. this._milliseconds += dur._milliseconds;
  19310. this._days += dur._days;
  19311. this._months += dur._months;
  19312. this._bubble();
  19313. return this;
  19314. },
  19315. subtract : function (input, val) {
  19316. var dur = moment.duration(input, val);
  19317. this._milliseconds -= dur._milliseconds;
  19318. this._days -= dur._days;
  19319. this._months -= dur._months;
  19320. this._bubble();
  19321. return this;
  19322. },
  19323. get : function (units) {
  19324. units = normalizeUnits(units);
  19325. return this[units.toLowerCase() + 's']();
  19326. },
  19327. as : function (units) {
  19328. units = normalizeUnits(units);
  19329. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  19330. },
  19331. lang : moment.fn.lang,
  19332. toIsoString : function () {
  19333. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  19334. var years = Math.abs(this.years()),
  19335. months = Math.abs(this.months()),
  19336. days = Math.abs(this.days()),
  19337. hours = Math.abs(this.hours()),
  19338. minutes = Math.abs(this.minutes()),
  19339. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  19340. if (!this.asSeconds()) {
  19341. // this is the same as C#'s (Noda) and python (isodate)...
  19342. // but not other JS (goog.date)
  19343. return 'P0D';
  19344. }
  19345. return (this.asSeconds() < 0 ? '-' : '') +
  19346. 'P' +
  19347. (years ? years + 'Y' : '') +
  19348. (months ? months + 'M' : '') +
  19349. (days ? days + 'D' : '') +
  19350. ((hours || minutes || seconds) ? 'T' : '') +
  19351. (hours ? hours + 'H' : '') +
  19352. (minutes ? minutes + 'M' : '') +
  19353. (seconds ? seconds + 'S' : '');
  19354. }
  19355. });
  19356. function makeDurationGetter(name) {
  19357. moment.duration.fn[name] = function () {
  19358. return this._data[name];
  19359. };
  19360. }
  19361. function makeDurationAsGetter(name, factor) {
  19362. moment.duration.fn['as' + name] = function () {
  19363. return +this / factor;
  19364. };
  19365. }
  19366. for (i in unitMillisecondFactors) {
  19367. if (unitMillisecondFactors.hasOwnProperty(i)) {
  19368. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  19369. makeDurationGetter(i.toLowerCase());
  19370. }
  19371. }
  19372. makeDurationAsGetter('Weeks', 6048e5);
  19373. moment.duration.fn.asMonths = function () {
  19374. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  19375. };
  19376. /************************************
  19377. Default Lang
  19378. ************************************/
  19379. // Set default language, other languages will inherit from English.
  19380. moment.lang('en', {
  19381. ordinal : function (number) {
  19382. var b = number % 10,
  19383. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  19384. (b === 1) ? 'st' :
  19385. (b === 2) ? 'nd' :
  19386. (b === 3) ? 'rd' : 'th';
  19387. return number + output;
  19388. }
  19389. });
  19390. /* EMBED_LANGUAGES */
  19391. /************************************
  19392. Exposing Moment
  19393. ************************************/
  19394. function makeGlobal(shouldDeprecate) {
  19395. /*global ender:false */
  19396. if (typeof ender !== 'undefined') {
  19397. return;
  19398. }
  19399. oldGlobalMoment = globalScope.moment;
  19400. if (shouldDeprecate) {
  19401. globalScope.moment = deprecate(
  19402. "Accessing Moment through the global scope is " +
  19403. "deprecated, and will be removed in an upcoming " +
  19404. "release.",
  19405. moment);
  19406. } else {
  19407. globalScope.moment = moment;
  19408. }
  19409. }
  19410. // CommonJS module is defined
  19411. if (hasModule) {
  19412. module.exports = moment;
  19413. } else if (typeof define === "function" && define.amd) {
  19414. define("moment", function (require, exports, module) {
  19415. if (module.config && module.config() && module.config().noGlobal === true) {
  19416. // release the global variable
  19417. globalScope.moment = oldGlobalMoment;
  19418. }
  19419. return moment;
  19420. });
  19421. makeGlobal(true);
  19422. } else {
  19423. makeGlobal();
  19424. }
  19425. }).call(this);
  19426. },{}],5:[function(require,module,exports){
  19427. /**
  19428. * Copyright 2012 Craig Campbell
  19429. *
  19430. * Licensed under the Apache License, Version 2.0 (the "License");
  19431. * you may not use this file except in compliance with the License.
  19432. * You may obtain a copy of the License at
  19433. *
  19434. * http://www.apache.org/licenses/LICENSE-2.0
  19435. *
  19436. * Unless required by applicable law or agreed to in writing, software
  19437. * distributed under the License is distributed on an "AS IS" BASIS,
  19438. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  19439. * See the License for the specific language governing permissions and
  19440. * limitations under the License.
  19441. *
  19442. * Mousetrap is a simple keyboard shortcut library for Javascript with
  19443. * no external dependencies
  19444. *
  19445. * @version 1.1.2
  19446. * @url craig.is/killing/mice
  19447. */
  19448. /**
  19449. * mapping of special keycodes to their corresponding keys
  19450. *
  19451. * everything in this dictionary cannot use keypress events
  19452. * so it has to be here to map to the correct keycodes for
  19453. * keyup/keydown events
  19454. *
  19455. * @type {Object}
  19456. */
  19457. var _MAP = {
  19458. 8: 'backspace',
  19459. 9: 'tab',
  19460. 13: 'enter',
  19461. 16: 'shift',
  19462. 17: 'ctrl',
  19463. 18: 'alt',
  19464. 20: 'capslock',
  19465. 27: 'esc',
  19466. 32: 'space',
  19467. 33: 'pageup',
  19468. 34: 'pagedown',
  19469. 35: 'end',
  19470. 36: 'home',
  19471. 37: 'left',
  19472. 38: 'up',
  19473. 39: 'right',
  19474. 40: 'down',
  19475. 45: 'ins',
  19476. 46: 'del',
  19477. 91: 'meta',
  19478. 93: 'meta',
  19479. 224: 'meta'
  19480. },
  19481. /**
  19482. * mapping for special characters so they can support
  19483. *
  19484. * this dictionary is only used incase you want to bind a
  19485. * keyup or keydown event to one of these keys
  19486. *
  19487. * @type {Object}
  19488. */
  19489. _KEYCODE_MAP = {
  19490. 106: '*',
  19491. 107: '+',
  19492. 109: '-',
  19493. 110: '.',
  19494. 111 : '/',
  19495. 186: ';',
  19496. 187: '=',
  19497. 188: ',',
  19498. 189: '-',
  19499. 190: '.',
  19500. 191: '/',
  19501. 192: '`',
  19502. 219: '[',
  19503. 220: '\\',
  19504. 221: ']',
  19505. 222: '\''
  19506. },
  19507. /**
  19508. * this is a mapping of keys that require shift on a US keypad
  19509. * back to the non shift equivelents
  19510. *
  19511. * this is so you can use keyup events with these keys
  19512. *
  19513. * note that this will only work reliably on US keyboards
  19514. *
  19515. * @type {Object}
  19516. */
  19517. _SHIFT_MAP = {
  19518. '~': '`',
  19519. '!': '1',
  19520. '@': '2',
  19521. '#': '3',
  19522. '$': '4',
  19523. '%': '5',
  19524. '^': '6',
  19525. '&': '7',
  19526. '*': '8',
  19527. '(': '9',
  19528. ')': '0',
  19529. '_': '-',
  19530. '+': '=',
  19531. ':': ';',
  19532. '\"': '\'',
  19533. '<': ',',
  19534. '>': '.',
  19535. '?': '/',
  19536. '|': '\\'
  19537. },
  19538. /**
  19539. * this is a list of special strings you can use to map
  19540. * to modifier keys when you specify your keyboard shortcuts
  19541. *
  19542. * @type {Object}
  19543. */
  19544. _SPECIAL_ALIASES = {
  19545. 'option': 'alt',
  19546. 'command': 'meta',
  19547. 'return': 'enter',
  19548. 'escape': 'esc'
  19549. },
  19550. /**
  19551. * variable to store the flipped version of _MAP from above
  19552. * needed to check if we should use keypress or not when no action
  19553. * is specified
  19554. *
  19555. * @type {Object|undefined}
  19556. */
  19557. _REVERSE_MAP,
  19558. /**
  19559. * a list of all the callbacks setup via Mousetrap.bind()
  19560. *
  19561. * @type {Object}
  19562. */
  19563. _callbacks = {},
  19564. /**
  19565. * direct map of string combinations to callbacks used for trigger()
  19566. *
  19567. * @type {Object}
  19568. */
  19569. _direct_map = {},
  19570. /**
  19571. * keeps track of what level each sequence is at since multiple
  19572. * sequences can start out with the same sequence
  19573. *
  19574. * @type {Object}
  19575. */
  19576. _sequence_levels = {},
  19577. /**
  19578. * variable to store the setTimeout call
  19579. *
  19580. * @type {null|number}
  19581. */
  19582. _reset_timer,
  19583. /**
  19584. * temporary state where we will ignore the next keyup
  19585. *
  19586. * @type {boolean|string}
  19587. */
  19588. _ignore_next_keyup = false,
  19589. /**
  19590. * are we currently inside of a sequence?
  19591. * type of action ("keyup" or "keydown" or "keypress") or false
  19592. *
  19593. * @type {boolean|string}
  19594. */
  19595. _inside_sequence = false;
  19596. /**
  19597. * loop through the f keys, f1 to f19 and add them to the map
  19598. * programatically
  19599. */
  19600. for (var i = 1; i < 20; ++i) {
  19601. _MAP[111 + i] = 'f' + i;
  19602. }
  19603. /**
  19604. * loop through to map numbers on the numeric keypad
  19605. */
  19606. for (i = 0; i <= 9; ++i) {
  19607. _MAP[i + 96] = i;
  19608. }
  19609. /**
  19610. * cross browser add event method
  19611. *
  19612. * @param {Element|HTMLDocument} object
  19613. * @param {string} type
  19614. * @param {Function} callback
  19615. * @returns void
  19616. */
  19617. function _addEvent(object, type, callback) {
  19618. if (object.addEventListener) {
  19619. return object.addEventListener(type, callback, false);
  19620. }
  19621. object.attachEvent('on' + type, callback);
  19622. }
  19623. /**
  19624. * takes the event and returns the key character
  19625. *
  19626. * @param {Event} e
  19627. * @return {string}
  19628. */
  19629. function _characterFromEvent(e) {
  19630. // for keypress events we should return the character as is
  19631. if (e.type == 'keypress') {
  19632. return String.fromCharCode(e.which);
  19633. }
  19634. // for non keypress events the special maps are needed
  19635. if (_MAP[e.which]) {
  19636. return _MAP[e.which];
  19637. }
  19638. if (_KEYCODE_MAP[e.which]) {
  19639. return _KEYCODE_MAP[e.which];
  19640. }
  19641. // if it is not in the special map
  19642. return String.fromCharCode(e.which).toLowerCase();
  19643. }
  19644. /**
  19645. * should we stop this event before firing off callbacks
  19646. *
  19647. * @param {Event} e
  19648. * @return {boolean}
  19649. */
  19650. function _stop(e) {
  19651. var element = e.target || e.srcElement,
  19652. tag_name = element.tagName;
  19653. // if the element has the class "mousetrap" then no need to stop
  19654. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  19655. return false;
  19656. }
  19657. // stop for input, select, and textarea
  19658. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  19659. }
  19660. /**
  19661. * checks if two arrays are equal
  19662. *
  19663. * @param {Array} modifiers1
  19664. * @param {Array} modifiers2
  19665. * @returns {boolean}
  19666. */
  19667. function _modifiersMatch(modifiers1, modifiers2) {
  19668. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  19669. }
  19670. /**
  19671. * resets all sequence counters except for the ones passed in
  19672. *
  19673. * @param {Object} do_not_reset
  19674. * @returns void
  19675. */
  19676. function _resetSequences(do_not_reset) {
  19677. do_not_reset = do_not_reset || {};
  19678. var active_sequences = false,
  19679. key;
  19680. for (key in _sequence_levels) {
  19681. if (do_not_reset[key]) {
  19682. active_sequences = true;
  19683. continue;
  19684. }
  19685. _sequence_levels[key] = 0;
  19686. }
  19687. if (!active_sequences) {
  19688. _inside_sequence = false;
  19689. }
  19690. }
  19691. /**
  19692. * finds all callbacks that match based on the keycode, modifiers,
  19693. * and action
  19694. *
  19695. * @param {string} character
  19696. * @param {Array} modifiers
  19697. * @param {string} action
  19698. * @param {boolean=} remove - should we remove any matches
  19699. * @param {string=} combination
  19700. * @returns {Array}
  19701. */
  19702. function _getMatches(character, modifiers, action, remove, combination) {
  19703. var i,
  19704. callback,
  19705. matches = [];
  19706. // if there are no events related to this keycode
  19707. if (!_callbacks[character]) {
  19708. return [];
  19709. }
  19710. // if a modifier key is coming up on its own we should allow it
  19711. if (action == 'keyup' && _isModifier(character)) {
  19712. modifiers = [character];
  19713. }
  19714. // loop through all callbacks for the key that was pressed
  19715. // and see if any of them match
  19716. for (i = 0; i < _callbacks[character].length; ++i) {
  19717. callback = _callbacks[character][i];
  19718. // if this is a sequence but it is not at the right level
  19719. // then move onto the next match
  19720. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  19721. continue;
  19722. }
  19723. // if the action we are looking for doesn't match the action we got
  19724. // then we should keep going
  19725. if (action != callback.action) {
  19726. continue;
  19727. }
  19728. // if this is a keypress event that means that we need to only
  19729. // look at the character, otherwise check the modifiers as
  19730. // well
  19731. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  19732. // remove is used so if you change your mind and call bind a
  19733. // second time with a new function the first one is overwritten
  19734. if (remove && callback.combo == combination) {
  19735. _callbacks[character].splice(i, 1);
  19736. }
  19737. matches.push(callback);
  19738. }
  19739. }
  19740. return matches;
  19741. }
  19742. /**
  19743. * takes a key event and figures out what the modifiers are
  19744. *
  19745. * @param {Event} e
  19746. * @returns {Array}
  19747. */
  19748. function _eventModifiers(e) {
  19749. var modifiers = [];
  19750. if (e.shiftKey) {
  19751. modifiers.push('shift');
  19752. }
  19753. if (e.altKey) {
  19754. modifiers.push('alt');
  19755. }
  19756. if (e.ctrlKey) {
  19757. modifiers.push('ctrl');
  19758. }
  19759. if (e.metaKey) {
  19760. modifiers.push('meta');
  19761. }
  19762. return modifiers;
  19763. }
  19764. /**
  19765. * actually calls the callback function
  19766. *
  19767. * if your callback function returns false this will use the jquery
  19768. * convention - prevent default and stop propogation on the event
  19769. *
  19770. * @param {Function} callback
  19771. * @param {Event} e
  19772. * @returns void
  19773. */
  19774. function _fireCallback(callback, e) {
  19775. if (callback(e) === false) {
  19776. if (e.preventDefault) {
  19777. e.preventDefault();
  19778. }
  19779. if (e.stopPropagation) {
  19780. e.stopPropagation();
  19781. }
  19782. e.returnValue = false;
  19783. e.cancelBubble = true;
  19784. }
  19785. }
  19786. /**
  19787. * handles a character key event
  19788. *
  19789. * @param {string} character
  19790. * @param {Event} e
  19791. * @returns void
  19792. */
  19793. function _handleCharacter(character, e) {
  19794. // if this event should not happen stop here
  19795. if (_stop(e)) {
  19796. return;
  19797. }
  19798. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  19799. i,
  19800. do_not_reset = {},
  19801. processed_sequence_callback = false;
  19802. // loop through matching callbacks for this key event
  19803. for (i = 0; i < callbacks.length; ++i) {
  19804. // fire for all sequence callbacks
  19805. // this is because if for example you have multiple sequences
  19806. // bound such as "g i" and "g t" they both need to fire the
  19807. // callback for matching g cause otherwise you can only ever
  19808. // match the first one
  19809. if (callbacks[i].seq) {
  19810. processed_sequence_callback = true;
  19811. // keep a list of which sequences were matches for later
  19812. do_not_reset[callbacks[i].seq] = 1;
  19813. _fireCallback(callbacks[i].callback, e);
  19814. continue;
  19815. }
  19816. // if there were no sequence matches but we are still here
  19817. // that means this is a regular match so we should fire that
  19818. if (!processed_sequence_callback && !_inside_sequence) {
  19819. _fireCallback(callbacks[i].callback, e);
  19820. }
  19821. }
  19822. // if you are inside of a sequence and the key you are pressing
  19823. // is not a modifier key then we should reset all sequences
  19824. // that were not matched by this key event
  19825. if (e.type == _inside_sequence && !_isModifier(character)) {
  19826. _resetSequences(do_not_reset);
  19827. }
  19828. }
  19829. /**
  19830. * handles a keydown event
  19831. *
  19832. * @param {Event} e
  19833. * @returns void
  19834. */
  19835. function _handleKey(e) {
  19836. // normalize e.which for key events
  19837. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  19838. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  19839. var character = _characterFromEvent(e);
  19840. // no character found then stop
  19841. if (!character) {
  19842. return;
  19843. }
  19844. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  19845. _ignore_next_keyup = false;
  19846. return;
  19847. }
  19848. _handleCharacter(character, e);
  19849. }
  19850. /**
  19851. * determines if the keycode specified is a modifier key or not
  19852. *
  19853. * @param {string} key
  19854. * @returns {boolean}
  19855. */
  19856. function _isModifier(key) {
  19857. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  19858. }
  19859. /**
  19860. * called to set a 1 second timeout on the specified sequence
  19861. *
  19862. * this is so after each key press in the sequence you have 1 second
  19863. * to press the next key before you have to start over
  19864. *
  19865. * @returns void
  19866. */
  19867. function _resetSequenceTimer() {
  19868. clearTimeout(_reset_timer);
  19869. _reset_timer = setTimeout(_resetSequences, 1000);
  19870. }
  19871. /**
  19872. * reverses the map lookup so that we can look for specific keys
  19873. * to see what can and can't use keypress
  19874. *
  19875. * @return {Object}
  19876. */
  19877. function _getReverseMap() {
  19878. if (!_REVERSE_MAP) {
  19879. _REVERSE_MAP = {};
  19880. for (var key in _MAP) {
  19881. // pull out the numeric keypad from here cause keypress should
  19882. // be able to detect the keys from the character
  19883. if (key > 95 && key < 112) {
  19884. continue;
  19885. }
  19886. if (_MAP.hasOwnProperty(key)) {
  19887. _REVERSE_MAP[_MAP[key]] = key;
  19888. }
  19889. }
  19890. }
  19891. return _REVERSE_MAP;
  19892. }
  19893. /**
  19894. * picks the best action based on the key combination
  19895. *
  19896. * @param {string} key - character for key
  19897. * @param {Array} modifiers
  19898. * @param {string=} action passed in
  19899. */
  19900. function _pickBestAction(key, modifiers, action) {
  19901. // if no action was picked in we should try to pick the one
  19902. // that we think would work best for this key
  19903. if (!action) {
  19904. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  19905. }
  19906. // modifier keys don't work as expected with keypress,
  19907. // switch to keydown
  19908. if (action == 'keypress' && modifiers.length) {
  19909. action = 'keydown';
  19910. }
  19911. return action;
  19912. }
  19913. /**
  19914. * binds a key sequence to an event
  19915. *
  19916. * @param {string} combo - combo specified in bind call
  19917. * @param {Array} keys
  19918. * @param {Function} callback
  19919. * @param {string=} action
  19920. * @returns void
  19921. */
  19922. function _bindSequence(combo, keys, callback, action) {
  19923. // start off by adding a sequence level record for this combination
  19924. // and setting the level to 0
  19925. _sequence_levels[combo] = 0;
  19926. // if there is no action pick the best one for the first key
  19927. // in the sequence
  19928. if (!action) {
  19929. action = _pickBestAction(keys[0], []);
  19930. }
  19931. /**
  19932. * callback to increase the sequence level for this sequence and reset
  19933. * all other sequences that were active
  19934. *
  19935. * @param {Event} e
  19936. * @returns void
  19937. */
  19938. var _increaseSequence = function(e) {
  19939. _inside_sequence = action;
  19940. ++_sequence_levels[combo];
  19941. _resetSequenceTimer();
  19942. },
  19943. /**
  19944. * wraps the specified callback inside of another function in order
  19945. * to reset all sequence counters as soon as this sequence is done
  19946. *
  19947. * @param {Event} e
  19948. * @returns void
  19949. */
  19950. _callbackAndReset = function(e) {
  19951. _fireCallback(callback, e);
  19952. // we should ignore the next key up if the action is key down
  19953. // or keypress. this is so if you finish a sequence and
  19954. // release the key the final key will not trigger a keyup
  19955. if (action !== 'keyup') {
  19956. _ignore_next_keyup = _characterFromEvent(e);
  19957. }
  19958. // weird race condition if a sequence ends with the key
  19959. // another sequence begins with
  19960. setTimeout(_resetSequences, 10);
  19961. },
  19962. i;
  19963. // loop through keys one at a time and bind the appropriate callback
  19964. // function. for any key leading up to the final one it should
  19965. // increase the sequence. after the final, it should reset all sequences
  19966. for (i = 0; i < keys.length; ++i) {
  19967. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  19968. }
  19969. }
  19970. /**
  19971. * binds a single keyboard combination
  19972. *
  19973. * @param {string} combination
  19974. * @param {Function} callback
  19975. * @param {string=} action
  19976. * @param {string=} sequence_name - name of sequence if part of sequence
  19977. * @param {number=} level - what part of the sequence the command is
  19978. * @returns void
  19979. */
  19980. function _bindSingle(combination, callback, action, sequence_name, level) {
  19981. // make sure multiple spaces in a row become a single space
  19982. combination = combination.replace(/\s+/g, ' ');
  19983. var sequence = combination.split(' '),
  19984. i,
  19985. key,
  19986. keys,
  19987. modifiers = [];
  19988. // if this pattern is a sequence of keys then run through this method
  19989. // to reprocess each pattern one key at a time
  19990. if (sequence.length > 1) {
  19991. return _bindSequence(combination, sequence, callback, action);
  19992. }
  19993. // take the keys from this pattern and figure out what the actual
  19994. // pattern is all about
  19995. keys = combination === '+' ? ['+'] : combination.split('+');
  19996. for (i = 0; i < keys.length; ++i) {
  19997. key = keys[i];
  19998. // normalize key names
  19999. if (_SPECIAL_ALIASES[key]) {
  20000. key = _SPECIAL_ALIASES[key];
  20001. }
  20002. // if this is not a keypress event then we should
  20003. // be smart about using shift keys
  20004. // this will only work for US keyboards however
  20005. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  20006. key = _SHIFT_MAP[key];
  20007. modifiers.push('shift');
  20008. }
  20009. // if this key is a modifier then add it to the list of modifiers
  20010. if (_isModifier(key)) {
  20011. modifiers.push(key);
  20012. }
  20013. }
  20014. // depending on what the key combination is
  20015. // we will try to pick the best event for it
  20016. action = _pickBestAction(key, modifiers, action);
  20017. // make sure to initialize array if this is the first time
  20018. // a callback is added for this key
  20019. if (!_callbacks[key]) {
  20020. _callbacks[key] = [];
  20021. }
  20022. // remove an existing match if there is one
  20023. _getMatches(key, modifiers, action, !sequence_name, combination);
  20024. // add this call back to the array
  20025. // if it is a sequence put it at the beginning
  20026. // if not put it at the end
  20027. //
  20028. // this is important because the way these are processed expects
  20029. // the sequence ones to come first
  20030. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  20031. callback: callback,
  20032. modifiers: modifiers,
  20033. action: action,
  20034. seq: sequence_name,
  20035. level: level,
  20036. combo: combination
  20037. });
  20038. }
  20039. /**
  20040. * binds multiple combinations to the same callback
  20041. *
  20042. * @param {Array} combinations
  20043. * @param {Function} callback
  20044. * @param {string|undefined} action
  20045. * @returns void
  20046. */
  20047. function _bindMultiple(combinations, callback, action) {
  20048. for (var i = 0; i < combinations.length; ++i) {
  20049. _bindSingle(combinations[i], callback, action);
  20050. }
  20051. }
  20052. // start!
  20053. _addEvent(document, 'keypress', _handleKey);
  20054. _addEvent(document, 'keydown', _handleKey);
  20055. _addEvent(document, 'keyup', _handleKey);
  20056. var mousetrap = {
  20057. /**
  20058. * binds an event to mousetrap
  20059. *
  20060. * can be a single key, a combination of keys separated with +,
  20061. * a comma separated list of keys, an array of keys, or
  20062. * a sequence of keys separated by spaces
  20063. *
  20064. * be sure to list the modifier keys first to make sure that the
  20065. * correct key ends up getting bound (the last key in the pattern)
  20066. *
  20067. * @param {string|Array} keys
  20068. * @param {Function} callback
  20069. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  20070. * @returns void
  20071. */
  20072. bind: function(keys, callback, action) {
  20073. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  20074. _direct_map[keys + ':' + action] = callback;
  20075. return this;
  20076. },
  20077. /**
  20078. * unbinds an event to mousetrap
  20079. *
  20080. * the unbinding sets the callback function of the specified key combo
  20081. * to an empty function and deletes the corresponding key in the
  20082. * _direct_map dict.
  20083. *
  20084. * the keycombo+action has to be exactly the same as
  20085. * it was defined in the bind method
  20086. *
  20087. * TODO: actually remove this from the _callbacks dictionary instead
  20088. * of binding an empty function
  20089. *
  20090. * @param {string|Array} keys
  20091. * @param {string} action
  20092. * @returns void
  20093. */
  20094. unbind: function(keys, action) {
  20095. if (_direct_map[keys + ':' + action]) {
  20096. delete _direct_map[keys + ':' + action];
  20097. this.bind(keys, function() {}, action);
  20098. }
  20099. return this;
  20100. },
  20101. /**
  20102. * triggers an event that has already been bound
  20103. *
  20104. * @param {string} keys
  20105. * @param {string=} action
  20106. * @returns void
  20107. */
  20108. trigger: function(keys, action) {
  20109. _direct_map[keys + ':' + action]();
  20110. return this;
  20111. },
  20112. /**
  20113. * resets the library back to its initial state. this is useful
  20114. * if you want to clear out the current keyboard shortcuts and bind
  20115. * new ones - for example if you switch to another page
  20116. *
  20117. * @returns void
  20118. */
  20119. reset: function() {
  20120. _callbacks = {};
  20121. _direct_map = {};
  20122. return this;
  20123. }
  20124. };
  20125. module.exports = mousetrap;
  20126. },{}]},{},[1])
  20127. (1)
  20128. });