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.

22674 lines
670 KiB

  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 1.0.1
  8. * @date 2014-05-09
  9. *
  10. * @license
  11. * Copyright (C) 2011-2014 Almende B.V, http://almende.com
  12. *
  13. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  14. * use this file except in compliance with the License. You may obtain a copy
  15. * of the License at
  16. *
  17. * http://www.apache.org/licenses/LICENSE-2.0
  18. *
  19. * Unless required by applicable law or agreed to in writing, software
  20. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  21. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  22. * License for the specific language governing permissions and limitations under
  23. * the License.
  24. */
  25. !function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.vis=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
  26. /**
  27. * vis.js module imports
  28. */
  29. // Try to load dependencies from the global window object.
  30. // If not available there, load via require.
  31. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  32. var Emitter = require('emitter-component');
  33. var Hammer;
  34. if (typeof window !== 'undefined') {
  35. // load hammer.js only when running in a browser (where window is available)
  36. Hammer = window['Hammer'] || require('hammerjs');
  37. }
  38. else {
  39. Hammer = function () {
  40. throw Error('hammer.js is only available in a browser, not in node.js.');
  41. }
  42. }
  43. var mousetrap;
  44. if (typeof window !== 'undefined') {
  45. // load mousetrap.js only when running in a browser (where window is available)
  46. mousetrap = window['mousetrap'] || require('mousetrap');
  47. }
  48. else {
  49. mousetrap = function () {
  50. throw Error('mouseTrap is only available in a browser, not in node.js.');
  51. }
  52. }
  53. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  54. // it here in that case.
  55. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  56. if(!Array.prototype.indexOf) {
  57. Array.prototype.indexOf = function(obj){
  58. for(var i = 0; i < this.length; i++){
  59. if(this[i] == obj){
  60. return i;
  61. }
  62. }
  63. return -1;
  64. };
  65. try {
  66. console.log("Warning: Ancient browser detected. Please update your browser");
  67. }
  68. catch (err) {
  69. }
  70. }
  71. // Internet Explorer 8 and older does not support Array.forEach, so we define
  72. // it here in that case.
  73. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  74. if (!Array.prototype.forEach) {
  75. Array.prototype.forEach = function(fn, scope) {
  76. for(var i = 0, len = this.length; i < len; ++i) {
  77. fn.call(scope || this, this[i], i, this);
  78. }
  79. }
  80. }
  81. // Internet Explorer 8 and older does not support Array.map, so we define it
  82. // here in that case.
  83. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  84. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  85. // Reference: http://es5.github.com/#x15.4.4.19
  86. if (!Array.prototype.map) {
  87. Array.prototype.map = function(callback, thisArg) {
  88. var T, A, k;
  89. if (this == null) {
  90. throw new TypeError(" this is null or not defined");
  91. }
  92. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  93. var O = Object(this);
  94. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  95. // 3. Let len be ToUint32(lenValue).
  96. var len = O.length >>> 0;
  97. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  98. // See: http://es5.github.com/#x9.11
  99. if (typeof callback !== "function") {
  100. throw new TypeError(callback + " is not a function");
  101. }
  102. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  103. if (thisArg) {
  104. T = thisArg;
  105. }
  106. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  107. // the standard built-in constructor with that name and len is the value of len.
  108. A = new Array(len);
  109. // 7. Let k be 0
  110. k = 0;
  111. // 8. Repeat, while k < len
  112. while(k < len) {
  113. var kValue, mappedValue;
  114. // a. Let Pk be ToString(k).
  115. // This is implicit for LHS operands of the in operator
  116. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  117. // This step can be combined with c
  118. // c. If kPresent is true, then
  119. if (k in O) {
  120. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  121. kValue = O[ k ];
  122. // ii. Let mappedValue be the result of calling the Call internal method of callback
  123. // with T as the this value and argument list containing kValue, k, and O.
  124. mappedValue = callback.call(T, kValue, k, O);
  125. // iii. Call the DefineOwnProperty internal method of A with arguments
  126. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  127. // and false.
  128. // In browsers that support Object.defineProperty, use the following:
  129. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  130. // For best browser support, use the following:
  131. A[ k ] = mappedValue;
  132. }
  133. // d. Increase k by 1.
  134. k++;
  135. }
  136. // 9. return A
  137. return A;
  138. };
  139. }
  140. // Internet Explorer 8 and older does not support Array.filter, so we define it
  141. // here in that case.
  142. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  143. if (!Array.prototype.filter) {
  144. Array.prototype.filter = function(fun /*, thisp */) {
  145. "use strict";
  146. if (this == null) {
  147. throw new TypeError();
  148. }
  149. var t = Object(this);
  150. var len = t.length >>> 0;
  151. if (typeof fun != "function") {
  152. throw new TypeError();
  153. }
  154. var res = [];
  155. var thisp = arguments[1];
  156. for (var i = 0; i < len; i++) {
  157. if (i in t) {
  158. var val = t[i]; // in case fun mutates this
  159. if (fun.call(thisp, val, i, t))
  160. res.push(val);
  161. }
  162. }
  163. return res;
  164. };
  165. }
  166. // Internet Explorer 8 and older does not support Object.keys, so we define it
  167. // here in that case.
  168. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  169. if (!Object.keys) {
  170. Object.keys = (function () {
  171. var hasOwnProperty = Object.prototype.hasOwnProperty,
  172. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  173. dontEnums = [
  174. 'toString',
  175. 'toLocaleString',
  176. 'valueOf',
  177. 'hasOwnProperty',
  178. 'isPrototypeOf',
  179. 'propertyIsEnumerable',
  180. 'constructor'
  181. ],
  182. dontEnumsLength = dontEnums.length;
  183. return function (obj) {
  184. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  185. throw new TypeError('Object.keys called on non-object');
  186. }
  187. var result = [];
  188. for (var prop in obj) {
  189. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  190. }
  191. if (hasDontEnumBug) {
  192. for (var i=0; i < dontEnumsLength; i++) {
  193. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  194. }
  195. }
  196. return result;
  197. }
  198. })()
  199. }
  200. // Internet Explorer 8 and older does not support Array.isArray,
  201. // so we define it here in that case.
  202. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  203. if(!Array.isArray) {
  204. Array.isArray = function (vArg) {
  205. return Object.prototype.toString.call(vArg) === "[object Array]";
  206. };
  207. }
  208. // Internet Explorer 8 and older does not support Function.bind,
  209. // so we define it here in that case.
  210. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  211. if (!Function.prototype.bind) {
  212. Function.prototype.bind = function (oThis) {
  213. if (typeof this !== "function") {
  214. // closest thing possible to the ECMAScript 5 internal IsCallable function
  215. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  216. }
  217. var aArgs = Array.prototype.slice.call(arguments, 1),
  218. fToBind = this,
  219. fNOP = function () {},
  220. fBound = function () {
  221. return fToBind.apply(this instanceof fNOP && oThis
  222. ? this
  223. : oThis,
  224. aArgs.concat(Array.prototype.slice.call(arguments)));
  225. };
  226. fNOP.prototype = this.prototype;
  227. fBound.prototype = new fNOP();
  228. return fBound;
  229. };
  230. }
  231. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  232. if (!Object.create) {
  233. Object.create = function (o) {
  234. if (arguments.length > 1) {
  235. throw new Error('Object.create implementation only accepts the first parameter.');
  236. }
  237. function F() {}
  238. F.prototype = o;
  239. return new F();
  240. };
  241. }
  242. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  243. if (!Function.prototype.bind) {
  244. Function.prototype.bind = function (oThis) {
  245. if (typeof this !== "function") {
  246. // closest thing possible to the ECMAScript 5 internal IsCallable function
  247. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  248. }
  249. var aArgs = Array.prototype.slice.call(arguments, 1),
  250. fToBind = this,
  251. fNOP = function () {},
  252. fBound = function () {
  253. return fToBind.apply(this instanceof fNOP && oThis
  254. ? this
  255. : oThis,
  256. aArgs.concat(Array.prototype.slice.call(arguments)));
  257. };
  258. fNOP.prototype = this.prototype;
  259. fBound.prototype = new fNOP();
  260. return fBound;
  261. };
  262. }
  263. /**
  264. * utility functions
  265. */
  266. var util = {};
  267. /**
  268. * Test whether given object is a number
  269. * @param {*} object
  270. * @return {Boolean} isNumber
  271. */
  272. util.isNumber = function isNumber(object) {
  273. return (object instanceof Number || typeof object == 'number');
  274. };
  275. /**
  276. * Test whether given object is a string
  277. * @param {*} object
  278. * @return {Boolean} isString
  279. */
  280. util.isString = function isString(object) {
  281. return (object instanceof String || typeof object == 'string');
  282. };
  283. /**
  284. * Test whether given object is a Date, or a String containing a Date
  285. * @param {Date | String} object
  286. * @return {Boolean} isDate
  287. */
  288. util.isDate = function isDate(object) {
  289. if (object instanceof Date) {
  290. return true;
  291. }
  292. else if (util.isString(object)) {
  293. // test whether this string contains a date
  294. var match = ASPDateRegex.exec(object);
  295. if (match) {
  296. return true;
  297. }
  298. else if (!isNaN(Date.parse(object))) {
  299. return true;
  300. }
  301. }
  302. return false;
  303. };
  304. /**
  305. * Test whether given object is an instance of google.visualization.DataTable
  306. * @param {*} object
  307. * @return {Boolean} isDataTable
  308. */
  309. util.isDataTable = function isDataTable(object) {
  310. return (typeof (google) !== 'undefined') &&
  311. (google.visualization) &&
  312. (google.visualization.DataTable) &&
  313. (object instanceof google.visualization.DataTable);
  314. };
  315. /**
  316. * Create a semi UUID
  317. * source: http://stackoverflow.com/a/105074/1262753
  318. * @return {String} uuid
  319. */
  320. util.randomUUID = function randomUUID () {
  321. var S4 = function () {
  322. return Math.floor(
  323. Math.random() * 0x10000 /* 65536 */
  324. ).toString(16);
  325. };
  326. return (
  327. S4() + S4() + '-' +
  328. S4() + '-' +
  329. S4() + '-' +
  330. S4() + '-' +
  331. S4() + S4() + S4()
  332. );
  333. };
  334. /**
  335. * Extend object a with the properties of object b or a series of objects
  336. * Only properties with defined values are copied
  337. * @param {Object} a
  338. * @param {... Object} b
  339. * @return {Object} a
  340. */
  341. util.extend = function (a, b) {
  342. for (var i = 1, len = arguments.length; i < len; i++) {
  343. var other = arguments[i];
  344. for (var prop in other) {
  345. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  346. a[prop] = other[prop];
  347. }
  348. }
  349. }
  350. return a;
  351. };
  352. /**
  353. * Test whether all elements in two arrays are equal.
  354. * @param {Array} a
  355. * @param {Array} b
  356. * @return {boolean} Returns true if both arrays have the same length and same
  357. * elements.
  358. */
  359. util.equalArray = function (a, b) {
  360. if (a.length != b.length) return false;
  361. for (var i = 0, len = a.length; i < len; i++) {
  362. if (a[i] != b[i]) return false;
  363. }
  364. return true;
  365. };
  366. /**
  367. * Convert an object to another type
  368. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  369. * @param {String | undefined} type Name of the type. Available types:
  370. * 'Boolean', 'Number', 'String',
  371. * 'Date', 'Moment', ISODate', 'ASPDate'.
  372. * @return {*} object
  373. * @throws Error
  374. */
  375. util.convert = function convert(object, type) {
  376. var match;
  377. if (object === undefined) {
  378. return undefined;
  379. }
  380. if (object === null) {
  381. return null;
  382. }
  383. if (!type) {
  384. return object;
  385. }
  386. if (!(typeof type === 'string') && !(type instanceof String)) {
  387. throw new Error('Type must be a string');
  388. }
  389. //noinspection FallthroughInSwitchStatementJS
  390. switch (type) {
  391. case 'boolean':
  392. case 'Boolean':
  393. return Boolean(object);
  394. case 'number':
  395. case 'Number':
  396. return Number(object.valueOf());
  397. case 'string':
  398. case 'String':
  399. return String(object);
  400. case 'Date':
  401. if (util.isNumber(object)) {
  402. return new Date(object);
  403. }
  404. if (object instanceof Date) {
  405. return new Date(object.valueOf());
  406. }
  407. else if (moment.isMoment(object)) {
  408. return new Date(object.valueOf());
  409. }
  410. if (util.isString(object)) {
  411. match = ASPDateRegex.exec(object);
  412. if (match) {
  413. // object is an ASP date
  414. return new Date(Number(match[1])); // parse number
  415. }
  416. else {
  417. return moment(object).toDate(); // parse string
  418. }
  419. }
  420. else {
  421. throw new Error(
  422. 'Cannot convert object of type ' + util.getType(object) +
  423. ' to type Date');
  424. }
  425. case 'Moment':
  426. if (util.isNumber(object)) {
  427. return moment(object);
  428. }
  429. if (object instanceof Date) {
  430. return moment(object.valueOf());
  431. }
  432. else if (moment.isMoment(object)) {
  433. return moment(object);
  434. }
  435. if (util.isString(object)) {
  436. match = ASPDateRegex.exec(object);
  437. if (match) {
  438. // object is an ASP date
  439. return moment(Number(match[1])); // parse number
  440. }
  441. else {
  442. return moment(object); // parse string
  443. }
  444. }
  445. else {
  446. throw new Error(
  447. 'Cannot convert object of type ' + util.getType(object) +
  448. ' to type Date');
  449. }
  450. case 'ISODate':
  451. if (util.isNumber(object)) {
  452. return new Date(object);
  453. }
  454. else if (object instanceof Date) {
  455. return object.toISOString();
  456. }
  457. else if (moment.isMoment(object)) {
  458. return object.toDate().toISOString();
  459. }
  460. else if (util.isString(object)) {
  461. match = ASPDateRegex.exec(object);
  462. if (match) {
  463. // object is an ASP date
  464. return new Date(Number(match[1])).toISOString(); // parse number
  465. }
  466. else {
  467. return new Date(object).toISOString(); // parse string
  468. }
  469. }
  470. else {
  471. throw new Error(
  472. 'Cannot convert object of type ' + util.getType(object) +
  473. ' to type ISODate');
  474. }
  475. case 'ASPDate':
  476. if (util.isNumber(object)) {
  477. return '/Date(' + object + ')/';
  478. }
  479. else if (object instanceof Date) {
  480. return '/Date(' + object.valueOf() + ')/';
  481. }
  482. else if (util.isString(object)) {
  483. match = ASPDateRegex.exec(object);
  484. var value;
  485. if (match) {
  486. // object is an ASP date
  487. value = new Date(Number(match[1])).valueOf(); // parse number
  488. }
  489. else {
  490. value = new Date(object).valueOf(); // parse string
  491. }
  492. return '/Date(' + value + ')/';
  493. }
  494. else {
  495. throw new Error(
  496. 'Cannot convert object of type ' + util.getType(object) +
  497. ' to type ASPDate');
  498. }
  499. default:
  500. throw new Error('Cannot convert object of type ' + util.getType(object) +
  501. ' to type "' + type + '"');
  502. }
  503. };
  504. // parse ASP.Net Date pattern,
  505. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  506. // code from http://momentjs.com/
  507. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  508. /**
  509. * Get the type of an object, for example util.getType([]) returns 'Array'
  510. * @param {*} object
  511. * @return {String} type
  512. */
  513. util.getType = function getType(object) {
  514. var type = typeof object;
  515. if (type == 'object') {
  516. if (object == null) {
  517. return 'null';
  518. }
  519. if (object instanceof Boolean) {
  520. return 'Boolean';
  521. }
  522. if (object instanceof Number) {
  523. return 'Number';
  524. }
  525. if (object instanceof String) {
  526. return 'String';
  527. }
  528. if (object instanceof Array) {
  529. return 'Array';
  530. }
  531. if (object instanceof Date) {
  532. return 'Date';
  533. }
  534. return 'Object';
  535. }
  536. else if (type == 'number') {
  537. return 'Number';
  538. }
  539. else if (type == 'boolean') {
  540. return 'Boolean';
  541. }
  542. else if (type == 'string') {
  543. return 'String';
  544. }
  545. return type;
  546. };
  547. /**
  548. * Retrieve the absolute left value of a DOM element
  549. * @param {Element} elem A dom element, for example a div
  550. * @return {number} left The absolute left position of this element
  551. * in the browser page.
  552. */
  553. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  554. var doc = document.documentElement;
  555. var body = document.body;
  556. var left = elem.offsetLeft;
  557. var e = elem.offsetParent;
  558. while (e != null && e != body && e != doc) {
  559. left += e.offsetLeft;
  560. left -= e.scrollLeft;
  561. e = e.offsetParent;
  562. }
  563. return left;
  564. };
  565. /**
  566. * Retrieve the absolute top value of a DOM element
  567. * @param {Element} elem A dom element, for example a div
  568. * @return {number} top The absolute top position of this element
  569. * in the browser page.
  570. */
  571. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  572. var doc = document.documentElement;
  573. var body = document.body;
  574. var top = elem.offsetTop;
  575. var e = elem.offsetParent;
  576. while (e != null && e != body && e != doc) {
  577. top += e.offsetTop;
  578. top -= e.scrollTop;
  579. e = e.offsetParent;
  580. }
  581. return top;
  582. };
  583. /**
  584. * Get the absolute, vertical mouse position from an event.
  585. * @param {Event} event
  586. * @return {Number} pageY
  587. */
  588. util.getPageY = function getPageY (event) {
  589. if ('pageY' in event) {
  590. return event.pageY;
  591. }
  592. else {
  593. var clientY;
  594. if (('targetTouches' in event) && event.targetTouches.length) {
  595. clientY = event.targetTouches[0].clientY;
  596. }
  597. else {
  598. clientY = event.clientY;
  599. }
  600. var doc = document.documentElement;
  601. var body = document.body;
  602. return clientY +
  603. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  604. ( doc && doc.clientTop || body && body.clientTop || 0 );
  605. }
  606. };
  607. /**
  608. * Get the absolute, horizontal mouse position from an event.
  609. * @param {Event} event
  610. * @return {Number} pageX
  611. */
  612. util.getPageX = function getPageX (event) {
  613. if ('pageY' in event) {
  614. return event.pageX;
  615. }
  616. else {
  617. var clientX;
  618. if (('targetTouches' in event) && event.targetTouches.length) {
  619. clientX = event.targetTouches[0].clientX;
  620. }
  621. else {
  622. clientX = event.clientX;
  623. }
  624. var doc = document.documentElement;
  625. var body = document.body;
  626. return clientX +
  627. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  628. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  629. }
  630. };
  631. /**
  632. * add a className to the given elements style
  633. * @param {Element} elem
  634. * @param {String} className
  635. */
  636. util.addClassName = function addClassName(elem, className) {
  637. var classes = elem.className.split(' ');
  638. if (classes.indexOf(className) == -1) {
  639. classes.push(className); // add the class to the array
  640. elem.className = classes.join(' ');
  641. }
  642. };
  643. /**
  644. * add a className to the given elements style
  645. * @param {Element} elem
  646. * @param {String} className
  647. */
  648. util.removeClassName = function removeClassname(elem, className) {
  649. var classes = elem.className.split(' ');
  650. var index = classes.indexOf(className);
  651. if (index != -1) {
  652. classes.splice(index, 1); // remove the class from the array
  653. elem.className = classes.join(' ');
  654. }
  655. };
  656. /**
  657. * For each method for both arrays and objects.
  658. * In case of an array, the built-in Array.forEach() is applied.
  659. * In case of an Object, the method loops over all properties of the object.
  660. * @param {Object | Array} object An Object or Array
  661. * @param {function} callback Callback method, called for each item in
  662. * the object or array with three parameters:
  663. * callback(value, index, object)
  664. */
  665. util.forEach = function forEach (object, callback) {
  666. var i,
  667. len;
  668. if (object instanceof Array) {
  669. // array
  670. for (i = 0, len = object.length; i < len; i++) {
  671. callback(object[i], i, object);
  672. }
  673. }
  674. else {
  675. // object
  676. for (i in object) {
  677. if (object.hasOwnProperty(i)) {
  678. callback(object[i], i, object);
  679. }
  680. }
  681. }
  682. };
  683. /**
  684. * Convert an object into an array: all objects properties are put into the
  685. * array. The resulting array is unordered.
  686. * @param {Object} object
  687. * @param {Array} array
  688. */
  689. util.toArray = function toArray(object) {
  690. var array = [];
  691. for (var prop in object) {
  692. if (object.hasOwnProperty(prop)) array.push(object[prop]);
  693. }
  694. return array;
  695. }
  696. /**
  697. * Update a property in an object
  698. * @param {Object} object
  699. * @param {String} key
  700. * @param {*} value
  701. * @return {Boolean} changed
  702. */
  703. util.updateProperty = function updateProperty (object, key, value) {
  704. if (object[key] !== value) {
  705. object[key] = value;
  706. return true;
  707. }
  708. else {
  709. return false;
  710. }
  711. };
  712. /**
  713. * Add and event listener. Works for all browsers
  714. * @param {Element} element An html element
  715. * @param {string} action The action, for example "click",
  716. * without the prefix "on"
  717. * @param {function} listener The callback function to be executed
  718. * @param {boolean} [useCapture]
  719. */
  720. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  721. if (element.addEventListener) {
  722. if (useCapture === undefined)
  723. useCapture = false;
  724. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  725. action = "DOMMouseScroll"; // For Firefox
  726. }
  727. element.addEventListener(action, listener, useCapture);
  728. } else {
  729. element.attachEvent("on" + action, listener); // IE browsers
  730. }
  731. };
  732. /**
  733. * Remove an event listener from an element
  734. * @param {Element} element An html dom element
  735. * @param {string} action The name of the event, for example "mousedown"
  736. * @param {function} listener The listener function
  737. * @param {boolean} [useCapture]
  738. */
  739. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  740. if (element.removeEventListener) {
  741. // non-IE browsers
  742. if (useCapture === undefined)
  743. useCapture = false;
  744. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  745. action = "DOMMouseScroll"; // For Firefox
  746. }
  747. element.removeEventListener(action, listener, useCapture);
  748. } else {
  749. // IE browsers
  750. element.detachEvent("on" + action, listener);
  751. }
  752. };
  753. /**
  754. * Get HTML element which is the target of the event
  755. * @param {Event} event
  756. * @return {Element} target element
  757. */
  758. util.getTarget = function getTarget(event) {
  759. // code from http://www.quirksmode.org/js/events_properties.html
  760. if (!event) {
  761. event = window.event;
  762. }
  763. var target;
  764. if (event.target) {
  765. target = event.target;
  766. }
  767. else if (event.srcElement) {
  768. target = event.srcElement;
  769. }
  770. if (target.nodeType != undefined && target.nodeType == 3) {
  771. // defeat Safari bug
  772. target = target.parentNode;
  773. }
  774. return target;
  775. };
  776. /**
  777. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  778. * @param {Element} element
  779. * @param {Event} event
  780. */
  781. util.fakeGesture = function fakeGesture (element, event) {
  782. var eventType = null;
  783. // for hammer.js 1.0.5
  784. var gesture = Hammer.event.collectEventData(this, eventType, event);
  785. // for hammer.js 1.0.6
  786. //var touches = Hammer.event.getTouchList(event, eventType);
  787. // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
  788. // on IE in standards mode, no touches are recognized by hammer.js,
  789. // resulting in NaN values for center.pageX and center.pageY
  790. if (isNaN(gesture.center.pageX)) {
  791. gesture.center.pageX = event.pageX;
  792. }
  793. if (isNaN(gesture.center.pageY)) {
  794. gesture.center.pageY = event.pageY;
  795. }
  796. return gesture;
  797. };
  798. util.option = {};
  799. /**
  800. * Convert a value into a boolean
  801. * @param {Boolean | function | undefined} value
  802. * @param {Boolean} [defaultValue]
  803. * @returns {Boolean} bool
  804. */
  805. util.option.asBoolean = function (value, defaultValue) {
  806. if (typeof value == 'function') {
  807. value = value();
  808. }
  809. if (value != null) {
  810. return (value != false);
  811. }
  812. return defaultValue || null;
  813. };
  814. /**
  815. * Convert a value into a number
  816. * @param {Boolean | function | undefined} value
  817. * @param {Number} [defaultValue]
  818. * @returns {Number} number
  819. */
  820. util.option.asNumber = function (value, defaultValue) {
  821. if (typeof value == 'function') {
  822. value = value();
  823. }
  824. if (value != null) {
  825. return Number(value) || defaultValue || null;
  826. }
  827. return defaultValue || null;
  828. };
  829. /**
  830. * Convert a value into a string
  831. * @param {String | function | undefined} value
  832. * @param {String} [defaultValue]
  833. * @returns {String} str
  834. */
  835. util.option.asString = function (value, defaultValue) {
  836. if (typeof value == 'function') {
  837. value = value();
  838. }
  839. if (value != null) {
  840. return String(value);
  841. }
  842. return defaultValue || null;
  843. };
  844. /**
  845. * Convert a size or location into a string with pixels or a percentage
  846. * @param {String | Number | function | undefined} value
  847. * @param {String} [defaultValue]
  848. * @returns {String} size
  849. */
  850. util.option.asSize = function (value, defaultValue) {
  851. if (typeof value == 'function') {
  852. value = value();
  853. }
  854. if (util.isString(value)) {
  855. return value;
  856. }
  857. else if (util.isNumber(value)) {
  858. return value + 'px';
  859. }
  860. else {
  861. return defaultValue || null;
  862. }
  863. };
  864. /**
  865. * Convert a value into a DOM element
  866. * @param {HTMLElement | function | undefined} value
  867. * @param {HTMLElement} [defaultValue]
  868. * @returns {HTMLElement | null} dom
  869. */
  870. util.option.asElement = function (value, defaultValue) {
  871. if (typeof value == 'function') {
  872. value = value();
  873. }
  874. return value || defaultValue || null;
  875. };
  876. util.GiveDec = function GiveDec(Hex) {
  877. var Value;
  878. if (Hex == "A")
  879. Value = 10;
  880. else if (Hex == "B")
  881. Value = 11;
  882. else if (Hex == "C")
  883. Value = 12;
  884. else if (Hex == "D")
  885. Value = 13;
  886. else if (Hex == "E")
  887. Value = 14;
  888. else if (Hex == "F")
  889. Value = 15;
  890. else
  891. Value = eval(Hex);
  892. return Value;
  893. };
  894. util.GiveHex = function GiveHex(Dec) {
  895. var Value;
  896. if(Dec == 10)
  897. Value = "A";
  898. else if (Dec == 11)
  899. Value = "B";
  900. else if (Dec == 12)
  901. Value = "C";
  902. else if (Dec == 13)
  903. Value = "D";
  904. else if (Dec == 14)
  905. Value = "E";
  906. else if (Dec == 15)
  907. Value = "F";
  908. else
  909. Value = "" + Dec;
  910. return Value;
  911. };
  912. /**
  913. * Parse a color property into an object with border, background, and
  914. * highlight colors
  915. * @param {Object | String} color
  916. * @return {Object} colorObject
  917. */
  918. util.parseColor = function(color) {
  919. var c;
  920. if (util.isString(color)) {
  921. if (util.isValidHex(color)) {
  922. var hsv = util.hexToHSV(color);
  923. var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
  924. var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
  925. var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
  926. var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
  927. c = {
  928. background: color,
  929. border:darkerColorHex,
  930. highlight: {
  931. background:lighterColorHex,
  932. border:darkerColorHex
  933. }
  934. };
  935. }
  936. else {
  937. c = {
  938. background:color,
  939. border:color,
  940. highlight: {
  941. background:color,
  942. border:color
  943. }
  944. };
  945. }
  946. }
  947. else {
  948. c = {};
  949. c.background = color.background || 'white';
  950. c.border = color.border || c.background;
  951. if (util.isString(color.highlight)) {
  952. c.highlight = {
  953. border: color.highlight,
  954. background: color.highlight
  955. }
  956. }
  957. else {
  958. c.highlight = {};
  959. c.highlight.background = color.highlight && color.highlight.background || c.background;
  960. c.highlight.border = color.highlight && color.highlight.border || c.border;
  961. }
  962. }
  963. return c;
  964. };
  965. /**
  966. * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
  967. *
  968. * @param {String} hex
  969. * @returns {{r: *, g: *, b: *}}
  970. */
  971. util.hexToRGB = function hexToRGB(hex) {
  972. hex = hex.replace("#","").toUpperCase();
  973. var a = util.GiveDec(hex.substring(0, 1));
  974. var b = util.GiveDec(hex.substring(1, 2));
  975. var c = util.GiveDec(hex.substring(2, 3));
  976. var d = util.GiveDec(hex.substring(3, 4));
  977. var e = util.GiveDec(hex.substring(4, 5));
  978. var f = util.GiveDec(hex.substring(5, 6));
  979. var r = (a * 16) + b;
  980. var g = (c * 16) + d;
  981. var b = (e * 16) + f;
  982. return {r:r,g:g,b:b};
  983. };
  984. util.RGBToHex = function RGBToHex(red,green,blue) {
  985. var a = util.GiveHex(Math.floor(red / 16));
  986. var b = util.GiveHex(red % 16);
  987. var c = util.GiveHex(Math.floor(green / 16));
  988. var d = util.GiveHex(green % 16);
  989. var e = util.GiveHex(Math.floor(blue / 16));
  990. var f = util.GiveHex(blue % 16);
  991. var hex = a + b + c + d + e + f;
  992. return "#" + hex;
  993. };
  994. /**
  995. * http://www.javascripter.net/faq/rgb2hsv.htm
  996. *
  997. * @param red
  998. * @param green
  999. * @param blue
  1000. * @returns {*}
  1001. * @constructor
  1002. */
  1003. util.RGBToHSV = function RGBToHSV (red,green,blue) {
  1004. red=red/255; green=green/255; blue=blue/255;
  1005. var minRGB = Math.min(red,Math.min(green,blue));
  1006. var maxRGB = Math.max(red,Math.max(green,blue));
  1007. // Black-gray-white
  1008. if (minRGB == maxRGB) {
  1009. return {h:0,s:0,v:minRGB};
  1010. }
  1011. // Colors other than black-gray-white:
  1012. var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
  1013. var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
  1014. var hue = 60*(h - d/(maxRGB - minRGB))/360;
  1015. var saturation = (maxRGB - minRGB)/maxRGB;
  1016. var value = maxRGB;
  1017. return {h:hue,s:saturation,v:value};
  1018. };
  1019. /**
  1020. * https://gist.github.com/mjijackson/5311256
  1021. * @param hue
  1022. * @param saturation
  1023. * @param value
  1024. * @returns {{r: number, g: number, b: number}}
  1025. * @constructor
  1026. */
  1027. util.HSVToRGB = function HSVToRGB(h, s, v) {
  1028. var r, g, b;
  1029. var i = Math.floor(h * 6);
  1030. var f = h * 6 - i;
  1031. var p = v * (1 - s);
  1032. var q = v * (1 - f * s);
  1033. var t = v * (1 - (1 - f) * s);
  1034. switch (i % 6) {
  1035. case 0: r = v, g = t, b = p; break;
  1036. case 1: r = q, g = v, b = p; break;
  1037. case 2: r = p, g = v, b = t; break;
  1038. case 3: r = p, g = q, b = v; break;
  1039. case 4: r = t, g = p, b = v; break;
  1040. case 5: r = v, g = p, b = q; break;
  1041. }
  1042. return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
  1043. };
  1044. util.HSVToHex = function HSVToHex(h, s, v) {
  1045. var rgb = util.HSVToRGB(h, s, v);
  1046. return util.RGBToHex(rgb.r, rgb.g, rgb.b);
  1047. };
  1048. util.hexToHSV = function hexToHSV(hex) {
  1049. var rgb = util.hexToRGB(hex);
  1050. return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
  1051. };
  1052. util.isValidHex = function isValidHex(hex) {
  1053. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  1054. return isOk;
  1055. };
  1056. util.copyObject = function copyObject(objectFrom, objectTo) {
  1057. for (var i in objectFrom) {
  1058. if (objectFrom.hasOwnProperty(i)) {
  1059. if (typeof objectFrom[i] == "object") {
  1060. objectTo[i] = {};
  1061. util.copyObject(objectFrom[i], objectTo[i]);
  1062. }
  1063. else {
  1064. objectTo[i] = objectFrom[i];
  1065. }
  1066. }
  1067. }
  1068. };
  1069. /**
  1070. * DataSet
  1071. *
  1072. * Usage:
  1073. * var dataSet = new DataSet({
  1074. * fieldId: '_id',
  1075. * convert: {
  1076. * // ...
  1077. * }
  1078. * });
  1079. *
  1080. * dataSet.add(item);
  1081. * dataSet.add(data);
  1082. * dataSet.update(item);
  1083. * dataSet.update(data);
  1084. * dataSet.remove(id);
  1085. * dataSet.remove(ids);
  1086. * var data = dataSet.get();
  1087. * var data = dataSet.get(id);
  1088. * var data = dataSet.get(ids);
  1089. * var data = dataSet.get(ids, options, data);
  1090. * dataSet.clear();
  1091. *
  1092. * A data set can:
  1093. * - add/remove/update data
  1094. * - gives triggers upon changes in the data
  1095. * - can import/export data in various data formats
  1096. *
  1097. * @param {Array | DataTable} [data] Optional array with initial data
  1098. * @param {Object} [options] Available options:
  1099. * {String} fieldId Field name of the id in the
  1100. * items, 'id' by default.
  1101. * {Object.<String, String} convert
  1102. * A map with field names as key,
  1103. * and the field type as value.
  1104. * @constructor DataSet
  1105. */
  1106. // TODO: add a DataSet constructor DataSet(data, options)
  1107. function DataSet (data, options) {
  1108. this.id = util.randomUUID();
  1109. // correctly read optional arguments
  1110. if (data && !Array.isArray(data) && !util.isDataTable(data)) {
  1111. options = data;
  1112. data = null;
  1113. }
  1114. this.options = options || {};
  1115. this.data = {}; // map with data indexed by id
  1116. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1117. this.convert = {}; // field types by field name
  1118. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1119. if (this.options.convert) {
  1120. for (var field in this.options.convert) {
  1121. if (this.options.convert.hasOwnProperty(field)) {
  1122. var value = this.options.convert[field];
  1123. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1124. this.convert[field] = 'Date';
  1125. }
  1126. else {
  1127. this.convert[field] = value;
  1128. }
  1129. }
  1130. }
  1131. }
  1132. this.subscribers = {}; // event subscribers
  1133. this.internalIds = {}; // internally generated id's
  1134. // add initial data when provided
  1135. if (data) {
  1136. this.add(data);
  1137. }
  1138. }
  1139. /**
  1140. * Subscribe to an event, add an event listener
  1141. * @param {String} event Event name. Available events: 'put', 'update',
  1142. * 'remove'
  1143. * @param {function} callback Callback method. Called with three parameters:
  1144. * {String} event
  1145. * {Object | null} params
  1146. * {String | Number} senderId
  1147. */
  1148. DataSet.prototype.on = function on (event, callback) {
  1149. var subscribers = this.subscribers[event];
  1150. if (!subscribers) {
  1151. subscribers = [];
  1152. this.subscribers[event] = subscribers;
  1153. }
  1154. subscribers.push({
  1155. callback: callback
  1156. });
  1157. };
  1158. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1159. DataSet.prototype.subscribe = DataSet.prototype.on;
  1160. /**
  1161. * Unsubscribe from an event, remove an event listener
  1162. * @param {String} event
  1163. * @param {function} callback
  1164. */
  1165. DataSet.prototype.off = function off(event, callback) {
  1166. var subscribers = this.subscribers[event];
  1167. if (subscribers) {
  1168. this.subscribers[event] = subscribers.filter(function (listener) {
  1169. return (listener.callback != callback);
  1170. });
  1171. }
  1172. };
  1173. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1174. DataSet.prototype.unsubscribe = DataSet.prototype.off;
  1175. /**
  1176. * Trigger an event
  1177. * @param {String} event
  1178. * @param {Object | null} params
  1179. * @param {String} [senderId] Optional id of the sender.
  1180. * @private
  1181. */
  1182. DataSet.prototype._trigger = function (event, params, senderId) {
  1183. if (event == '*') {
  1184. throw new Error('Cannot trigger event *');
  1185. }
  1186. var subscribers = [];
  1187. if (event in this.subscribers) {
  1188. subscribers = subscribers.concat(this.subscribers[event]);
  1189. }
  1190. if ('*' in this.subscribers) {
  1191. subscribers = subscribers.concat(this.subscribers['*']);
  1192. }
  1193. for (var i = 0; i < subscribers.length; i++) {
  1194. var subscriber = subscribers[i];
  1195. if (subscriber.callback) {
  1196. subscriber.callback(event, params, senderId || null);
  1197. }
  1198. }
  1199. };
  1200. /**
  1201. * Add data.
  1202. * Adding an item will fail when there already is an item with the same id.
  1203. * @param {Object | Array | DataTable} data
  1204. * @param {String} [senderId] Optional sender id
  1205. * @return {Array} addedIds Array with the ids of the added items
  1206. */
  1207. DataSet.prototype.add = function (data, senderId) {
  1208. var addedIds = [],
  1209. id,
  1210. me = this;
  1211. if (data instanceof Array) {
  1212. // Array
  1213. for (var i = 0, len = data.length; i < len; i++) {
  1214. id = me._addItem(data[i]);
  1215. addedIds.push(id);
  1216. }
  1217. }
  1218. else if (util.isDataTable(data)) {
  1219. // Google DataTable
  1220. var columns = this._getColumnNames(data);
  1221. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1222. var item = {};
  1223. for (var col = 0, cols = columns.length; col < cols; col++) {
  1224. var field = columns[col];
  1225. item[field] = data.getValue(row, col);
  1226. }
  1227. id = me._addItem(item);
  1228. addedIds.push(id);
  1229. }
  1230. }
  1231. else if (data instanceof Object) {
  1232. // Single item
  1233. id = me._addItem(data);
  1234. addedIds.push(id);
  1235. }
  1236. else {
  1237. throw new Error('Unknown dataType');
  1238. }
  1239. if (addedIds.length) {
  1240. this._trigger('add', {items: addedIds}, senderId);
  1241. }
  1242. return addedIds;
  1243. };
  1244. /**
  1245. * Update existing items. When an item does not exist, it will be created
  1246. * @param {Object | Array | DataTable} data
  1247. * @param {String} [senderId] Optional sender id
  1248. * @return {Array} updatedIds The ids of the added or updated items
  1249. */
  1250. DataSet.prototype.update = function (data, senderId) {
  1251. var addedIds = [],
  1252. updatedIds = [],
  1253. me = this,
  1254. fieldId = me.fieldId;
  1255. var addOrUpdate = function (item) {
  1256. var id = item[fieldId];
  1257. if (me.data[id]) {
  1258. // update item
  1259. id = me._updateItem(item);
  1260. updatedIds.push(id);
  1261. }
  1262. else {
  1263. // add new item
  1264. id = me._addItem(item);
  1265. addedIds.push(id);
  1266. }
  1267. };
  1268. if (data instanceof Array) {
  1269. // Array
  1270. for (var i = 0, len = data.length; i < len; i++) {
  1271. addOrUpdate(data[i]);
  1272. }
  1273. }
  1274. else if (util.isDataTable(data)) {
  1275. // Google DataTable
  1276. var columns = this._getColumnNames(data);
  1277. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1278. var item = {};
  1279. for (var col = 0, cols = columns.length; col < cols; col++) {
  1280. var field = columns[col];
  1281. item[field] = data.getValue(row, col);
  1282. }
  1283. addOrUpdate(item);
  1284. }
  1285. }
  1286. else if (data instanceof Object) {
  1287. // Single item
  1288. addOrUpdate(data);
  1289. }
  1290. else {
  1291. throw new Error('Unknown dataType');
  1292. }
  1293. if (addedIds.length) {
  1294. this._trigger('add', {items: addedIds}, senderId);
  1295. }
  1296. if (updatedIds.length) {
  1297. this._trigger('update', {items: updatedIds}, senderId);
  1298. }
  1299. return addedIds.concat(updatedIds);
  1300. };
  1301. /**
  1302. * Get a data item or multiple items.
  1303. *
  1304. * Usage:
  1305. *
  1306. * get()
  1307. * get(options: Object)
  1308. * get(options: Object, data: Array | DataTable)
  1309. *
  1310. * get(id: Number | String)
  1311. * get(id: Number | String, options: Object)
  1312. * get(id: Number | String, options: Object, data: Array | DataTable)
  1313. *
  1314. * get(ids: Number[] | String[])
  1315. * get(ids: Number[] | String[], options: Object)
  1316. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1317. *
  1318. * Where:
  1319. *
  1320. * {Number | String} id The id of an item
  1321. * {Number[] | String{}} ids An array with ids of items
  1322. * {Object} options An Object with options. Available options:
  1323. * {String} [type] Type of data to be returned. Can
  1324. * be 'DataTable' or 'Array' (default)
  1325. * {Object.<String, String>} [convert]
  1326. * {String[]} [fields] field names to be returned
  1327. * {function} [filter] filter items
  1328. * {String | function} [order] Order the items by
  1329. * a field name or custom sort function.
  1330. * {Array | DataTable} [data] If provided, items will be appended to this
  1331. * array or table. Required in case of Google
  1332. * DataTable.
  1333. *
  1334. * @throws Error
  1335. */
  1336. DataSet.prototype.get = function (args) {
  1337. var me = this;
  1338. var globalShowInternalIds = this.showInternalIds;
  1339. // parse the arguments
  1340. var id, ids, options, data;
  1341. var firstType = util.getType(arguments[0]);
  1342. if (firstType == 'String' || firstType == 'Number') {
  1343. // get(id [, options] [, data])
  1344. id = arguments[0];
  1345. options = arguments[1];
  1346. data = arguments[2];
  1347. }
  1348. else if (firstType == 'Array') {
  1349. // get(ids [, options] [, data])
  1350. ids = arguments[0];
  1351. options = arguments[1];
  1352. data = arguments[2];
  1353. }
  1354. else {
  1355. // get([, options] [, data])
  1356. options = arguments[0];
  1357. data = arguments[1];
  1358. }
  1359. // determine the return type
  1360. var type;
  1361. if (options && options.type) {
  1362. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1363. if (data && (type != util.getType(data))) {
  1364. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1365. 'does not correspond with specified options.type (' + options.type + ')');
  1366. }
  1367. if (type == 'DataTable' && !util.isDataTable(data)) {
  1368. throw new Error('Parameter "data" must be a DataTable ' +
  1369. 'when options.type is "DataTable"');
  1370. }
  1371. }
  1372. else if (data) {
  1373. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1374. }
  1375. else {
  1376. type = 'Array';
  1377. }
  1378. // we allow the setting of this value for a single get request.
  1379. if (options != undefined) {
  1380. if (options.showInternalIds != undefined) {
  1381. this.showInternalIds = options.showInternalIds;
  1382. }
  1383. }
  1384. // build options
  1385. var convert = options && options.convert || this.options.convert;
  1386. var filter = options && options.filter;
  1387. var items = [], item, itemId, i, len;
  1388. // convert items
  1389. if (id != undefined) {
  1390. // return a single item
  1391. item = me._getItem(id, convert);
  1392. if (filter && !filter(item)) {
  1393. item = null;
  1394. }
  1395. }
  1396. else if (ids != undefined) {
  1397. // return a subset of items
  1398. for (i = 0, len = ids.length; i < len; i++) {
  1399. item = me._getItem(ids[i], convert);
  1400. if (!filter || filter(item)) {
  1401. items.push(item);
  1402. }
  1403. }
  1404. }
  1405. else {
  1406. // return all items
  1407. for (itemId in this.data) {
  1408. if (this.data.hasOwnProperty(itemId)) {
  1409. item = me._getItem(itemId, convert);
  1410. if (!filter || filter(item)) {
  1411. items.push(item);
  1412. }
  1413. }
  1414. }
  1415. }
  1416. // restore the global value of showInternalIds
  1417. this.showInternalIds = globalShowInternalIds;
  1418. // order the results
  1419. if (options && options.order && id == undefined) {
  1420. this._sort(items, options.order);
  1421. }
  1422. // filter fields of the items
  1423. if (options && options.fields) {
  1424. var fields = options.fields;
  1425. if (id != undefined) {
  1426. item = this._filterFields(item, fields);
  1427. }
  1428. else {
  1429. for (i = 0, len = items.length; i < len; i++) {
  1430. items[i] = this._filterFields(items[i], fields);
  1431. }
  1432. }
  1433. }
  1434. // return the results
  1435. if (type == 'DataTable') {
  1436. var columns = this._getColumnNames(data);
  1437. if (id != undefined) {
  1438. // append a single item to the data table
  1439. me._appendRow(data, columns, item);
  1440. }
  1441. else {
  1442. // copy the items to the provided data table
  1443. for (i = 0, len = items.length; i < len; i++) {
  1444. me._appendRow(data, columns, items[i]);
  1445. }
  1446. }
  1447. return data;
  1448. }
  1449. else {
  1450. // return an array
  1451. if (id != undefined) {
  1452. // a single item
  1453. return item;
  1454. }
  1455. else {
  1456. // multiple items
  1457. if (data) {
  1458. // copy the items to the provided array
  1459. for (i = 0, len = items.length; i < len; i++) {
  1460. data.push(items[i]);
  1461. }
  1462. return data;
  1463. }
  1464. else {
  1465. // just return our array
  1466. return items;
  1467. }
  1468. }
  1469. }
  1470. };
  1471. /**
  1472. * Get ids of all items or from a filtered set of items.
  1473. * @param {Object} [options] An Object with options. Available options:
  1474. * {function} [filter] filter items
  1475. * {String | function} [order] Order the items by
  1476. * a field name or custom sort function.
  1477. * @return {Array} ids
  1478. */
  1479. DataSet.prototype.getIds = function (options) {
  1480. var data = this.data,
  1481. filter = options && options.filter,
  1482. order = options && options.order,
  1483. convert = options && options.convert || this.options.convert,
  1484. i,
  1485. len,
  1486. id,
  1487. item,
  1488. items,
  1489. ids = [];
  1490. if (filter) {
  1491. // get filtered items
  1492. if (order) {
  1493. // create ordered list
  1494. items = [];
  1495. for (id in data) {
  1496. if (data.hasOwnProperty(id)) {
  1497. item = this._getItem(id, convert);
  1498. if (filter(item)) {
  1499. items.push(item);
  1500. }
  1501. }
  1502. }
  1503. this._sort(items, order);
  1504. for (i = 0, len = items.length; i < len; i++) {
  1505. ids[i] = items[i][this.fieldId];
  1506. }
  1507. }
  1508. else {
  1509. // create unordered list
  1510. for (id in data) {
  1511. if (data.hasOwnProperty(id)) {
  1512. item = this._getItem(id, convert);
  1513. if (filter(item)) {
  1514. ids.push(item[this.fieldId]);
  1515. }
  1516. }
  1517. }
  1518. }
  1519. }
  1520. else {
  1521. // get all items
  1522. if (order) {
  1523. // create an ordered list
  1524. items = [];
  1525. for (id in data) {
  1526. if (data.hasOwnProperty(id)) {
  1527. items.push(data[id]);
  1528. }
  1529. }
  1530. this._sort(items, order);
  1531. for (i = 0, len = items.length; i < len; i++) {
  1532. ids[i] = items[i][this.fieldId];
  1533. }
  1534. }
  1535. else {
  1536. // create unordered list
  1537. for (id in data) {
  1538. if (data.hasOwnProperty(id)) {
  1539. item = data[id];
  1540. ids.push(item[this.fieldId]);
  1541. }
  1542. }
  1543. }
  1544. }
  1545. return ids;
  1546. };
  1547. /**
  1548. * Execute a callback function for every item in the dataset.
  1549. * @param {function} callback
  1550. * @param {Object} [options] Available options:
  1551. * {Object.<String, String>} [convert]
  1552. * {String[]} [fields] filter fields
  1553. * {function} [filter] filter items
  1554. * {String | function} [order] Order the items by
  1555. * a field name or custom sort function.
  1556. */
  1557. DataSet.prototype.forEach = function (callback, options) {
  1558. var filter = options && options.filter,
  1559. convert = options && options.convert || this.options.convert,
  1560. data = this.data,
  1561. item,
  1562. id;
  1563. if (options && options.order) {
  1564. // execute forEach on ordered list
  1565. var items = this.get(options);
  1566. for (var i = 0, len = items.length; i < len; i++) {
  1567. item = items[i];
  1568. id = item[this.fieldId];
  1569. callback(item, id);
  1570. }
  1571. }
  1572. else {
  1573. // unordered
  1574. for (id in data) {
  1575. if (data.hasOwnProperty(id)) {
  1576. item = this._getItem(id, convert);
  1577. if (!filter || filter(item)) {
  1578. callback(item, id);
  1579. }
  1580. }
  1581. }
  1582. }
  1583. };
  1584. /**
  1585. * Map every item in the dataset.
  1586. * @param {function} callback
  1587. * @param {Object} [options] Available options:
  1588. * {Object.<String, String>} [convert]
  1589. * {String[]} [fields] filter fields
  1590. * {function} [filter] filter items
  1591. * {String | function} [order] Order the items by
  1592. * a field name or custom sort function.
  1593. * @return {Object[]} mappedItems
  1594. */
  1595. DataSet.prototype.map = function (callback, options) {
  1596. var filter = options && options.filter,
  1597. convert = options && options.convert || this.options.convert,
  1598. mappedItems = [],
  1599. data = this.data,
  1600. item;
  1601. // convert and filter items
  1602. for (var id in data) {
  1603. if (data.hasOwnProperty(id)) {
  1604. item = this._getItem(id, convert);
  1605. if (!filter || filter(item)) {
  1606. mappedItems.push(callback(item, id));
  1607. }
  1608. }
  1609. }
  1610. // order items
  1611. if (options && options.order) {
  1612. this._sort(mappedItems, options.order);
  1613. }
  1614. return mappedItems;
  1615. };
  1616. /**
  1617. * Filter the fields of an item
  1618. * @param {Object} item
  1619. * @param {String[]} fields Field names
  1620. * @return {Object} filteredItem
  1621. * @private
  1622. */
  1623. DataSet.prototype._filterFields = function (item, fields) {
  1624. var filteredItem = {};
  1625. for (var field in item) {
  1626. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1627. filteredItem[field] = item[field];
  1628. }
  1629. }
  1630. return filteredItem;
  1631. };
  1632. /**
  1633. * Sort the provided array with items
  1634. * @param {Object[]} items
  1635. * @param {String | function} order A field name or custom sort function.
  1636. * @private
  1637. */
  1638. DataSet.prototype._sort = function (items, order) {
  1639. if (util.isString(order)) {
  1640. // order by provided field name
  1641. var name = order; // field name
  1642. items.sort(function (a, b) {
  1643. var av = a[name];
  1644. var bv = b[name];
  1645. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1646. });
  1647. }
  1648. else if (typeof order === 'function') {
  1649. // order by sort function
  1650. items.sort(order);
  1651. }
  1652. // TODO: extend order by an Object {field:String, direction:String}
  1653. // where direction can be 'asc' or 'desc'
  1654. else {
  1655. throw new TypeError('Order must be a function or a string');
  1656. }
  1657. };
  1658. /**
  1659. * Remove an object by pointer or by id
  1660. * @param {String | Number | Object | Array} id Object or id, or an array with
  1661. * objects or ids to be removed
  1662. * @param {String} [senderId] Optional sender id
  1663. * @return {Array} removedIds
  1664. */
  1665. DataSet.prototype.remove = function (id, senderId) {
  1666. var removedIds = [],
  1667. i, len, removedId;
  1668. if (id instanceof Array) {
  1669. for (i = 0, len = id.length; i < len; i++) {
  1670. removedId = this._remove(id[i]);
  1671. if (removedId != null) {
  1672. removedIds.push(removedId);
  1673. }
  1674. }
  1675. }
  1676. else {
  1677. removedId = this._remove(id);
  1678. if (removedId != null) {
  1679. removedIds.push(removedId);
  1680. }
  1681. }
  1682. if (removedIds.length) {
  1683. this._trigger('remove', {items: removedIds}, senderId);
  1684. }
  1685. return removedIds;
  1686. };
  1687. /**
  1688. * Remove an item by its id
  1689. * @param {Number | String | Object} id id or item
  1690. * @returns {Number | String | null} id
  1691. * @private
  1692. */
  1693. DataSet.prototype._remove = function (id) {
  1694. if (util.isNumber(id) || util.isString(id)) {
  1695. if (this.data[id]) {
  1696. delete this.data[id];
  1697. delete this.internalIds[id];
  1698. return id;
  1699. }
  1700. }
  1701. else if (id instanceof Object) {
  1702. var itemId = id[this.fieldId];
  1703. if (itemId && this.data[itemId]) {
  1704. delete this.data[itemId];
  1705. delete this.internalIds[itemId];
  1706. return itemId;
  1707. }
  1708. }
  1709. return null;
  1710. };
  1711. /**
  1712. * Clear the data
  1713. * @param {String} [senderId] Optional sender id
  1714. * @return {Array} removedIds The ids of all removed items
  1715. */
  1716. DataSet.prototype.clear = function (senderId) {
  1717. var ids = Object.keys(this.data);
  1718. this.data = {};
  1719. this.internalIds = {};
  1720. this._trigger('remove', {items: ids}, senderId);
  1721. return ids;
  1722. };
  1723. /**
  1724. * Find the item with maximum value of a specified field
  1725. * @param {String} field
  1726. * @return {Object | null} item Item containing max value, or null if no items
  1727. */
  1728. DataSet.prototype.max = function (field) {
  1729. var data = this.data,
  1730. max = null,
  1731. maxField = null;
  1732. for (var id in data) {
  1733. if (data.hasOwnProperty(id)) {
  1734. var item = data[id];
  1735. var itemField = item[field];
  1736. if (itemField != null && (!max || itemField > maxField)) {
  1737. max = item;
  1738. maxField = itemField;
  1739. }
  1740. }
  1741. }
  1742. return max;
  1743. };
  1744. /**
  1745. * Find the item with minimum value of a specified field
  1746. * @param {String} field
  1747. * @return {Object | null} item Item containing max value, or null if no items
  1748. */
  1749. DataSet.prototype.min = function (field) {
  1750. var data = this.data,
  1751. min = null,
  1752. minField = null;
  1753. for (var id in data) {
  1754. if (data.hasOwnProperty(id)) {
  1755. var item = data[id];
  1756. var itemField = item[field];
  1757. if (itemField != null && (!min || itemField < minField)) {
  1758. min = item;
  1759. minField = itemField;
  1760. }
  1761. }
  1762. }
  1763. return min;
  1764. };
  1765. /**
  1766. * Find all distinct values of a specified field
  1767. * @param {String} field
  1768. * @return {Array} values Array containing all distinct values. If data items
  1769. * do not contain the specified field are ignored.
  1770. * The returned array is unordered.
  1771. */
  1772. DataSet.prototype.distinct = function (field) {
  1773. var data = this.data,
  1774. values = [],
  1775. fieldType = this.options.convert[field],
  1776. count = 0;
  1777. for (var prop in data) {
  1778. if (data.hasOwnProperty(prop)) {
  1779. var item = data[prop];
  1780. var value = util.convert(item[field], fieldType);
  1781. var exists = false;
  1782. for (var i = 0; i < count; i++) {
  1783. if (values[i] == value) {
  1784. exists = true;
  1785. break;
  1786. }
  1787. }
  1788. if (!exists && (value !== undefined)) {
  1789. values[count] = value;
  1790. count++;
  1791. }
  1792. }
  1793. }
  1794. return values;
  1795. };
  1796. /**
  1797. * Add a single item. Will fail when an item with the same id already exists.
  1798. * @param {Object} item
  1799. * @return {String} id
  1800. * @private
  1801. */
  1802. DataSet.prototype._addItem = function (item) {
  1803. var id = item[this.fieldId];
  1804. if (id != undefined) {
  1805. // check whether this id is already taken
  1806. if (this.data[id]) {
  1807. // item already exists
  1808. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1809. }
  1810. }
  1811. else {
  1812. // generate an id
  1813. id = util.randomUUID();
  1814. item[this.fieldId] = id;
  1815. this.internalIds[id] = item;
  1816. }
  1817. var d = {};
  1818. for (var field in item) {
  1819. if (item.hasOwnProperty(field)) {
  1820. var fieldType = this.convert[field]; // type may be undefined
  1821. d[field] = util.convert(item[field], fieldType);
  1822. }
  1823. }
  1824. this.data[id] = d;
  1825. return id;
  1826. };
  1827. /**
  1828. * Get an item. Fields can be converted to a specific type
  1829. * @param {String} id
  1830. * @param {Object.<String, String>} [convert] field types to convert
  1831. * @return {Object | null} item
  1832. * @private
  1833. */
  1834. DataSet.prototype._getItem = function (id, convert) {
  1835. var field, value;
  1836. // get the item from the dataset
  1837. var raw = this.data[id];
  1838. if (!raw) {
  1839. return null;
  1840. }
  1841. // convert the items field types
  1842. var converted = {},
  1843. fieldId = this.fieldId,
  1844. internalIds = this.internalIds;
  1845. if (convert) {
  1846. for (field in raw) {
  1847. if (raw.hasOwnProperty(field)) {
  1848. value = raw[field];
  1849. // output all fields, except internal ids
  1850. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1851. converted[field] = util.convert(value, convert[field]);
  1852. }
  1853. }
  1854. }
  1855. }
  1856. else {
  1857. // no field types specified, no converting needed
  1858. for (field in raw) {
  1859. if (raw.hasOwnProperty(field)) {
  1860. value = raw[field];
  1861. // output all fields, except internal ids
  1862. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1863. converted[field] = value;
  1864. }
  1865. }
  1866. }
  1867. }
  1868. return converted;
  1869. };
  1870. /**
  1871. * Update a single item: merge with existing item.
  1872. * Will fail when the item has no id, or when there does not exist an item
  1873. * with the same id.
  1874. * @param {Object} item
  1875. * @return {String} id
  1876. * @private
  1877. */
  1878. DataSet.prototype._updateItem = function (item) {
  1879. var id = item[this.fieldId];
  1880. if (id == undefined) {
  1881. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1882. }
  1883. var d = this.data[id];
  1884. if (!d) {
  1885. // item doesn't exist
  1886. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1887. }
  1888. // merge with current item
  1889. for (var field in item) {
  1890. if (item.hasOwnProperty(field)) {
  1891. var fieldType = this.convert[field]; // type may be undefined
  1892. d[field] = util.convert(item[field], fieldType);
  1893. }
  1894. }
  1895. return id;
  1896. };
  1897. /**
  1898. * check if an id is an internal or external id
  1899. * @param id
  1900. * @returns {boolean}
  1901. * @private
  1902. */
  1903. DataSet.prototype.isInternalId = function(id) {
  1904. return (id in this.internalIds);
  1905. };
  1906. /**
  1907. * Get an array with the column names of a Google DataTable
  1908. * @param {DataTable} dataTable
  1909. * @return {String[]} columnNames
  1910. * @private
  1911. */
  1912. DataSet.prototype._getColumnNames = function (dataTable) {
  1913. var columns = [];
  1914. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1915. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1916. }
  1917. return columns;
  1918. };
  1919. /**
  1920. * Append an item as a row to the dataTable
  1921. * @param dataTable
  1922. * @param columns
  1923. * @param item
  1924. * @private
  1925. */
  1926. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1927. var row = dataTable.addRow();
  1928. for (var col = 0, cols = columns.length; col < cols; col++) {
  1929. var field = columns[col];
  1930. dataTable.setValue(row, col, item[field]);
  1931. }
  1932. };
  1933. /**
  1934. * DataView
  1935. *
  1936. * a dataview offers a filtered view on a dataset or an other dataview.
  1937. *
  1938. * @param {DataSet | DataView} data
  1939. * @param {Object} [options] Available options: see method get
  1940. *
  1941. * @constructor DataView
  1942. */
  1943. function DataView (data, options) {
  1944. this.id = util.randomUUID();
  1945. this.data = null;
  1946. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1947. this.options = options || {};
  1948. this.fieldId = 'id'; // name of the field containing id
  1949. this.subscribers = {}; // event subscribers
  1950. var me = this;
  1951. this.listener = function () {
  1952. me._onEvent.apply(me, arguments);
  1953. };
  1954. this.setData(data);
  1955. }
  1956. // TODO: implement a function .config() to dynamically update things like configured filter
  1957. // and trigger changes accordingly
  1958. /**
  1959. * Set a data source for the view
  1960. * @param {DataSet | DataView} data
  1961. */
  1962. DataView.prototype.setData = function (data) {
  1963. var ids, dataItems, i, len;
  1964. if (this.data) {
  1965. // unsubscribe from current dataset
  1966. if (this.data.unsubscribe) {
  1967. this.data.unsubscribe('*', this.listener);
  1968. }
  1969. // trigger a remove of all items in memory
  1970. ids = [];
  1971. for (var id in this.ids) {
  1972. if (this.ids.hasOwnProperty(id)) {
  1973. ids.push(id);
  1974. }
  1975. }
  1976. this.ids = {};
  1977. this._trigger('remove', {items: ids});
  1978. }
  1979. this.data = data;
  1980. if (this.data) {
  1981. // update fieldId
  1982. this.fieldId = this.options.fieldId ||
  1983. (this.data && this.data.options && this.data.options.fieldId) ||
  1984. 'id';
  1985. // trigger an add of all added items
  1986. ids = this.data.getIds({filter: this.options && this.options.filter});
  1987. for (i = 0, len = ids.length; i < len; i++) {
  1988. id = ids[i];
  1989. this.ids[id] = true;
  1990. }
  1991. this._trigger('add', {items: ids});
  1992. // subscribe to new dataset
  1993. if (this.data.on) {
  1994. this.data.on('*', this.listener);
  1995. }
  1996. }
  1997. };
  1998. /**
  1999. * Get data from the data view
  2000. *
  2001. * Usage:
  2002. *
  2003. * get()
  2004. * get(options: Object)
  2005. * get(options: Object, data: Array | DataTable)
  2006. *
  2007. * get(id: Number)
  2008. * get(id: Number, options: Object)
  2009. * get(id: Number, options: Object, data: Array | DataTable)
  2010. *
  2011. * get(ids: Number[])
  2012. * get(ids: Number[], options: Object)
  2013. * get(ids: Number[], options: Object, data: Array | DataTable)
  2014. *
  2015. * Where:
  2016. *
  2017. * {Number | String} id The id of an item
  2018. * {Number[] | String{}} ids An array with ids of items
  2019. * {Object} options An Object with options. Available options:
  2020. * {String} [type] Type of data to be returned. Can
  2021. * be 'DataTable' or 'Array' (default)
  2022. * {Object.<String, String>} [convert]
  2023. * {String[]} [fields] field names to be returned
  2024. * {function} [filter] filter items
  2025. * {String | function} [order] Order the items by
  2026. * a field name or custom sort function.
  2027. * {Array | DataTable} [data] If provided, items will be appended to this
  2028. * array or table. Required in case of Google
  2029. * DataTable.
  2030. * @param args
  2031. */
  2032. DataView.prototype.get = function (args) {
  2033. var me = this;
  2034. // parse the arguments
  2035. var ids, options, data;
  2036. var firstType = util.getType(arguments[0]);
  2037. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2038. // get(id(s) [, options] [, data])
  2039. ids = arguments[0]; // can be a single id or an array with ids
  2040. options = arguments[1];
  2041. data = arguments[2];
  2042. }
  2043. else {
  2044. // get([, options] [, data])
  2045. options = arguments[0];
  2046. data = arguments[1];
  2047. }
  2048. // extend the options with the default options and provided options
  2049. var viewOptions = util.extend({}, this.options, options);
  2050. // create a combined filter method when needed
  2051. if (this.options.filter && options && options.filter) {
  2052. viewOptions.filter = function (item) {
  2053. return me.options.filter(item) && options.filter(item);
  2054. }
  2055. }
  2056. // build up the call to the linked data set
  2057. var getArguments = [];
  2058. if (ids != undefined) {
  2059. getArguments.push(ids);
  2060. }
  2061. getArguments.push(viewOptions);
  2062. getArguments.push(data);
  2063. return this.data && this.data.get.apply(this.data, getArguments);
  2064. };
  2065. /**
  2066. * Get ids of all items or from a filtered set of items.
  2067. * @param {Object} [options] An Object with options. Available options:
  2068. * {function} [filter] filter items
  2069. * {String | function} [order] Order the items by
  2070. * a field name or custom sort function.
  2071. * @return {Array} ids
  2072. */
  2073. DataView.prototype.getIds = function (options) {
  2074. var ids;
  2075. if (this.data) {
  2076. var defaultFilter = this.options.filter;
  2077. var filter;
  2078. if (options && options.filter) {
  2079. if (defaultFilter) {
  2080. filter = function (item) {
  2081. return defaultFilter(item) && options.filter(item);
  2082. }
  2083. }
  2084. else {
  2085. filter = options.filter;
  2086. }
  2087. }
  2088. else {
  2089. filter = defaultFilter;
  2090. }
  2091. ids = this.data.getIds({
  2092. filter: filter,
  2093. order: options && options.order
  2094. });
  2095. }
  2096. else {
  2097. ids = [];
  2098. }
  2099. return ids;
  2100. };
  2101. /**
  2102. * Event listener. Will propagate all events from the connected data set to
  2103. * the subscribers of the DataView, but will filter the items and only trigger
  2104. * when there are changes in the filtered data set.
  2105. * @param {String} event
  2106. * @param {Object | null} params
  2107. * @param {String} senderId
  2108. * @private
  2109. */
  2110. DataView.prototype._onEvent = function (event, params, senderId) {
  2111. var i, len, id, item,
  2112. ids = params && params.items,
  2113. data = this.data,
  2114. added = [],
  2115. updated = [],
  2116. removed = [];
  2117. if (ids && data) {
  2118. switch (event) {
  2119. case 'add':
  2120. // filter the ids of the added items
  2121. for (i = 0, len = ids.length; i < len; i++) {
  2122. id = ids[i];
  2123. item = this.get(id);
  2124. if (item) {
  2125. this.ids[id] = true;
  2126. added.push(id);
  2127. }
  2128. }
  2129. break;
  2130. case 'update':
  2131. // determine the event from the views viewpoint: an updated
  2132. // item can be added, updated, or removed from this view.
  2133. for (i = 0, len = ids.length; i < len; i++) {
  2134. id = ids[i];
  2135. item = this.get(id);
  2136. if (item) {
  2137. if (this.ids[id]) {
  2138. updated.push(id);
  2139. }
  2140. else {
  2141. this.ids[id] = true;
  2142. added.push(id);
  2143. }
  2144. }
  2145. else {
  2146. if (this.ids[id]) {
  2147. delete this.ids[id];
  2148. removed.push(id);
  2149. }
  2150. else {
  2151. // nothing interesting for me :-(
  2152. }
  2153. }
  2154. }
  2155. break;
  2156. case 'remove':
  2157. // filter the ids of the removed items
  2158. for (i = 0, len = ids.length; i < len; i++) {
  2159. id = ids[i];
  2160. if (this.ids[id]) {
  2161. delete this.ids[id];
  2162. removed.push(id);
  2163. }
  2164. }
  2165. break;
  2166. }
  2167. if (added.length) {
  2168. this._trigger('add', {items: added}, senderId);
  2169. }
  2170. if (updated.length) {
  2171. this._trigger('update', {items: updated}, senderId);
  2172. }
  2173. if (removed.length) {
  2174. this._trigger('remove', {items: removed}, senderId);
  2175. }
  2176. }
  2177. };
  2178. // copy subscription functionality from DataSet
  2179. DataView.prototype.on = DataSet.prototype.on;
  2180. DataView.prototype.off = DataSet.prototype.off;
  2181. DataView.prototype._trigger = DataSet.prototype._trigger;
  2182. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2183. DataView.prototype.subscribe = DataView.prototype.on;
  2184. DataView.prototype.unsubscribe = DataView.prototype.off;
  2185. /**
  2186. * Utility functions for ordering and stacking of items
  2187. */
  2188. var stack = {};
  2189. /**
  2190. * Order items by their start data
  2191. * @param {Item[]} items
  2192. */
  2193. stack.orderByStart = function orderByStart(items) {
  2194. items.sort(function (a, b) {
  2195. return a.data.start - b.data.start;
  2196. });
  2197. };
  2198. /**
  2199. * Order items by their end date. If they have no end date, their start date
  2200. * is used.
  2201. * @param {Item[]} items
  2202. */
  2203. stack.orderByEnd = function orderByEnd(items) {
  2204. items.sort(function (a, b) {
  2205. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  2206. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  2207. return aTime - bTime;
  2208. });
  2209. };
  2210. /**
  2211. * Adjust vertical positions of the items such that they don't overlap each
  2212. * other.
  2213. * @param {Item[]} items
  2214. * All visible items
  2215. * @param {{item: number, axis: number}} margin
  2216. * Margins between items and between items and the axis.
  2217. * @param {boolean} [force=false]
  2218. * If true, all items will be repositioned. If false (default), only
  2219. * items having a top===null will be re-stacked
  2220. */
  2221. stack.stack = function _stack (items, margin, force) {
  2222. var i, iMax;
  2223. if (force) {
  2224. // reset top position of all items
  2225. for (i = 0, iMax = items.length; i < iMax; i++) {
  2226. items[i].top = null;
  2227. }
  2228. }
  2229. // calculate new, non-overlapping positions
  2230. for (i = 0, iMax = items.length; i < iMax; i++) {
  2231. var item = items[i];
  2232. if (item.top === null) {
  2233. // initialize top position
  2234. item.top = margin.axis;
  2235. do {
  2236. // TODO: optimize checking for overlap. when there is a gap without items,
  2237. // you only need to check for items from the next item on, not from zero
  2238. var collidingItem = null;
  2239. for (var j = 0, jj = items.length; j < jj; j++) {
  2240. var other = items[j];
  2241. if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) {
  2242. collidingItem = other;
  2243. break;
  2244. }
  2245. }
  2246. if (collidingItem != null) {
  2247. // There is a collision. Reposition the items above the colliding element
  2248. item.top = collidingItem.top + collidingItem.height + margin.item;
  2249. }
  2250. } while (collidingItem);
  2251. }
  2252. }
  2253. };
  2254. /**
  2255. * Adjust vertical positions of the items without stacking them
  2256. * @param {Item[]} items
  2257. * All visible items
  2258. * @param {{item: number, axis: number}} margin
  2259. * Margins between items and between items and the axis.
  2260. */
  2261. stack.nostack = function nostack (items, margin) {
  2262. var i, iMax;
  2263. // reset top position of all items
  2264. for (i = 0, iMax = items.length; i < iMax; i++) {
  2265. items[i].top = margin.axis;
  2266. }
  2267. };
  2268. /**
  2269. * Test if the two provided items collide
  2270. * The items must have parameters left, width, top, and height.
  2271. * @param {Item} a The first item
  2272. * @param {Item} b The second item
  2273. * @param {Number} margin A minimum required margin.
  2274. * If margin is provided, the two items will be
  2275. * marked colliding when they overlap or
  2276. * when the margin between the two is smaller than
  2277. * the requested margin.
  2278. * @return {boolean} true if a and b collide, else false
  2279. */
  2280. stack.collision = function collision (a, b, margin) {
  2281. return ((a.left - margin) < (b.left + b.width) &&
  2282. (a.left + a.width + margin) > b.left &&
  2283. (a.top - margin) < (b.top + b.height) &&
  2284. (a.top + a.height + margin) > b.top);
  2285. };
  2286. /**
  2287. * @constructor TimeStep
  2288. * The class TimeStep is an iterator for dates. You provide a start date and an
  2289. * end date. The class itself determines the best scale (step size) based on the
  2290. * provided start Date, end Date, and minimumStep.
  2291. *
  2292. * If minimumStep is provided, the step size is chosen as close as possible
  2293. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2294. * provided, the scale is set to 1 DAY.
  2295. * The minimumStep should correspond with the onscreen size of about 6 characters
  2296. *
  2297. * Alternatively, you can set a scale by hand.
  2298. * After creation, you can initialize the class by executing first(). Then you
  2299. * can iterate from the start date to the end date via next(). You can check if
  2300. * the end date is reached with the function hasNext(). After each step, you can
  2301. * retrieve the current date via getCurrent().
  2302. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2303. * days, to years.
  2304. *
  2305. * Version: 1.2
  2306. *
  2307. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2308. * or new Date(2010, 9, 21, 23, 45, 00)
  2309. * @param {Date} [end] The end date
  2310. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2311. */
  2312. function TimeStep(start, end, minimumStep) {
  2313. // variables
  2314. this.current = new Date();
  2315. this._start = new Date();
  2316. this._end = new Date();
  2317. this.autoScale = true;
  2318. this.scale = TimeStep.SCALE.DAY;
  2319. this.step = 1;
  2320. // initialize the range
  2321. this.setRange(start, end, minimumStep);
  2322. }
  2323. /// enum scale
  2324. TimeStep.SCALE = {
  2325. MILLISECOND: 1,
  2326. SECOND: 2,
  2327. MINUTE: 3,
  2328. HOUR: 4,
  2329. DAY: 5,
  2330. WEEKDAY: 6,
  2331. MONTH: 7,
  2332. YEAR: 8
  2333. };
  2334. /**
  2335. * Set a new range
  2336. * If minimumStep is provided, the step size is chosen as close as possible
  2337. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2338. * provided, the scale is set to 1 DAY.
  2339. * The minimumStep should correspond with the onscreen size of about 6 characters
  2340. * @param {Date} [start] The start date and time.
  2341. * @param {Date} [end] The end date and time.
  2342. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2343. */
  2344. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2345. if (!(start instanceof Date) || !(end instanceof Date)) {
  2346. throw "No legal start or end date in method setRange";
  2347. }
  2348. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2349. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2350. if (this.autoScale) {
  2351. this.setMinimumStep(minimumStep);
  2352. }
  2353. };
  2354. /**
  2355. * Set the range iterator to the start date.
  2356. */
  2357. TimeStep.prototype.first = function() {
  2358. this.current = new Date(this._start.valueOf());
  2359. this.roundToMinor();
  2360. };
  2361. /**
  2362. * Round the current date to the first minor date value
  2363. * This must be executed once when the current date is set to start Date
  2364. */
  2365. TimeStep.prototype.roundToMinor = function() {
  2366. // round to floor
  2367. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2368. //noinspection FallthroughInSwitchStatementJS
  2369. switch (this.scale) {
  2370. case TimeStep.SCALE.YEAR:
  2371. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2372. this.current.setMonth(0);
  2373. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2374. case TimeStep.SCALE.DAY: // intentional fall through
  2375. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2376. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2377. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2378. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2379. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2380. }
  2381. if (this.step != 1) {
  2382. // round down to the first minor value that is a multiple of the current step size
  2383. switch (this.scale) {
  2384. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2385. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2386. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2387. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2388. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2389. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2390. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2391. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2392. default: break;
  2393. }
  2394. }
  2395. };
  2396. /**
  2397. * Check if the there is a next step
  2398. * @return {boolean} true if the current date has not passed the end date
  2399. */
  2400. TimeStep.prototype.hasNext = function () {
  2401. return (this.current.valueOf() <= this._end.valueOf());
  2402. };
  2403. /**
  2404. * Do the next step
  2405. */
  2406. TimeStep.prototype.next = function() {
  2407. var prev = this.current.valueOf();
  2408. // Two cases, needed to prevent issues with switching daylight savings
  2409. // (end of March and end of October)
  2410. if (this.current.getMonth() < 6) {
  2411. switch (this.scale) {
  2412. case TimeStep.SCALE.MILLISECOND:
  2413. this.current = new Date(this.current.valueOf() + this.step); break;
  2414. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2415. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2416. case TimeStep.SCALE.HOUR:
  2417. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2418. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2419. var h = this.current.getHours();
  2420. this.current.setHours(h - (h % this.step));
  2421. break;
  2422. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2423. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2424. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2425. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2426. default: break;
  2427. }
  2428. }
  2429. else {
  2430. switch (this.scale) {
  2431. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2432. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2433. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2434. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2435. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2436. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2437. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2438. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2439. default: break;
  2440. }
  2441. }
  2442. if (this.step != 1) {
  2443. // round down to the correct major value
  2444. switch (this.scale) {
  2445. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2446. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2447. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2448. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2449. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2450. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2451. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2452. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2453. default: break;
  2454. }
  2455. }
  2456. // safety mechanism: if current time is still unchanged, move to the end
  2457. if (this.current.valueOf() == prev) {
  2458. this.current = new Date(this._end.valueOf());
  2459. }
  2460. };
  2461. /**
  2462. * Get the current datetime
  2463. * @return {Date} current The current date
  2464. */
  2465. TimeStep.prototype.getCurrent = function() {
  2466. return this.current;
  2467. };
  2468. /**
  2469. * Set a custom scale. Autoscaling will be disabled.
  2470. * For example setScale(SCALE.MINUTES, 5) will result
  2471. * in minor steps of 5 minutes, and major steps of an hour.
  2472. *
  2473. * @param {TimeStep.SCALE} newScale
  2474. * A scale. Choose from SCALE.MILLISECOND,
  2475. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2476. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2477. * SCALE.YEAR.
  2478. * @param {Number} newStep A step size, by default 1. Choose for
  2479. * example 1, 2, 5, or 10.
  2480. */
  2481. TimeStep.prototype.setScale = function(newScale, newStep) {
  2482. this.scale = newScale;
  2483. if (newStep > 0) {
  2484. this.step = newStep;
  2485. }
  2486. this.autoScale = false;
  2487. };
  2488. /**
  2489. * Enable or disable autoscaling
  2490. * @param {boolean} enable If true, autoascaling is set true
  2491. */
  2492. TimeStep.prototype.setAutoScale = function (enable) {
  2493. this.autoScale = enable;
  2494. };
  2495. /**
  2496. * Automatically determine the scale that bests fits the provided minimum step
  2497. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2498. */
  2499. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2500. if (minimumStep == undefined) {
  2501. return;
  2502. }
  2503. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2504. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2505. var stepDay = (1000 * 60 * 60 * 24);
  2506. var stepHour = (1000 * 60 * 60);
  2507. var stepMinute = (1000 * 60);
  2508. var stepSecond = (1000);
  2509. var stepMillisecond= (1);
  2510. // find the smallest step that is larger than the provided minimumStep
  2511. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2512. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2513. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2514. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2515. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2516. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2517. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2518. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2519. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2520. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2521. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2522. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2523. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2524. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2525. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2526. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2527. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2528. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2529. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2530. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2531. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2532. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2533. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2534. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2535. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2536. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2537. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2538. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2539. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2540. };
  2541. /**
  2542. * Snap a date to a rounded value.
  2543. * The snap intervals are dependent on the current scale and step.
  2544. * @param {Date} date the date to be snapped.
  2545. * @return {Date} snappedDate
  2546. */
  2547. TimeStep.prototype.snap = function(date) {
  2548. var clone = new Date(date.valueOf());
  2549. if (this.scale == TimeStep.SCALE.YEAR) {
  2550. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  2551. clone.setFullYear(Math.round(year / this.step) * this.step);
  2552. clone.setMonth(0);
  2553. clone.setDate(0);
  2554. clone.setHours(0);
  2555. clone.setMinutes(0);
  2556. clone.setSeconds(0);
  2557. clone.setMilliseconds(0);
  2558. }
  2559. else if (this.scale == TimeStep.SCALE.MONTH) {
  2560. if (clone.getDate() > 15) {
  2561. clone.setDate(1);
  2562. clone.setMonth(clone.getMonth() + 1);
  2563. // important: first set Date to 1, after that change the month.
  2564. }
  2565. else {
  2566. clone.setDate(1);
  2567. }
  2568. clone.setHours(0);
  2569. clone.setMinutes(0);
  2570. clone.setSeconds(0);
  2571. clone.setMilliseconds(0);
  2572. }
  2573. else if (this.scale == TimeStep.SCALE.DAY ||
  2574. this.scale == TimeStep.SCALE.WEEKDAY) {
  2575. //noinspection FallthroughInSwitchStatementJS
  2576. switch (this.step) {
  2577. case 5:
  2578. case 2:
  2579. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  2580. default:
  2581. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2582. }
  2583. clone.setMinutes(0);
  2584. clone.setSeconds(0);
  2585. clone.setMilliseconds(0);
  2586. }
  2587. else if (this.scale == TimeStep.SCALE.HOUR) {
  2588. switch (this.step) {
  2589. case 4:
  2590. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  2591. default:
  2592. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  2593. }
  2594. clone.setSeconds(0);
  2595. clone.setMilliseconds(0);
  2596. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2597. //noinspection FallthroughInSwitchStatementJS
  2598. switch (this.step) {
  2599. case 15:
  2600. case 10:
  2601. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  2602. clone.setSeconds(0);
  2603. break;
  2604. case 5:
  2605. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  2606. default:
  2607. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  2608. }
  2609. clone.setMilliseconds(0);
  2610. }
  2611. else if (this.scale == TimeStep.SCALE.SECOND) {
  2612. //noinspection FallthroughInSwitchStatementJS
  2613. switch (this.step) {
  2614. case 15:
  2615. case 10:
  2616. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  2617. clone.setMilliseconds(0);
  2618. break;
  2619. case 5:
  2620. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  2621. default:
  2622. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  2623. }
  2624. }
  2625. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2626. var step = this.step > 5 ? this.step / 2 : 1;
  2627. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  2628. }
  2629. return clone;
  2630. };
  2631. /**
  2632. * Check if the current value is a major value (for example when the step
  2633. * is DAY, a major value is each first day of the MONTH)
  2634. * @return {boolean} true if current date is major, else false.
  2635. */
  2636. TimeStep.prototype.isMajor = function() {
  2637. switch (this.scale) {
  2638. case TimeStep.SCALE.MILLISECOND:
  2639. return (this.current.getMilliseconds() == 0);
  2640. case TimeStep.SCALE.SECOND:
  2641. return (this.current.getSeconds() == 0);
  2642. case TimeStep.SCALE.MINUTE:
  2643. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2644. // Note: this is no bug. Major label is equal for both minute and hour scale
  2645. case TimeStep.SCALE.HOUR:
  2646. return (this.current.getHours() == 0);
  2647. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2648. case TimeStep.SCALE.DAY:
  2649. return (this.current.getDate() == 1);
  2650. case TimeStep.SCALE.MONTH:
  2651. return (this.current.getMonth() == 0);
  2652. case TimeStep.SCALE.YEAR:
  2653. return false;
  2654. default:
  2655. return false;
  2656. }
  2657. };
  2658. /**
  2659. * Returns formatted text for the minor axislabel, depending on the current
  2660. * date and the scale. For example when scale is MINUTE, the current time is
  2661. * formatted as "hh:mm".
  2662. * @param {Date} [date] custom date. if not provided, current date is taken
  2663. */
  2664. TimeStep.prototype.getLabelMinor = function(date) {
  2665. if (date == undefined) {
  2666. date = this.current;
  2667. }
  2668. switch (this.scale) {
  2669. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2670. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2671. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2672. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2673. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2674. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2675. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2676. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2677. default: return '';
  2678. }
  2679. };
  2680. /**
  2681. * Returns formatted text for the major axis label, depending on the current
  2682. * date and the scale. For example when scale is MINUTE, the major scale is
  2683. * hours, and the hour will be formatted as "hh".
  2684. * @param {Date} [date] custom date. if not provided, current date is taken
  2685. */
  2686. TimeStep.prototype.getLabelMajor = function(date) {
  2687. if (date == undefined) {
  2688. date = this.current;
  2689. }
  2690. //noinspection FallthroughInSwitchStatementJS
  2691. switch (this.scale) {
  2692. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2693. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2694. case TimeStep.SCALE.MINUTE:
  2695. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2696. case TimeStep.SCALE.WEEKDAY:
  2697. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2698. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2699. case TimeStep.SCALE.YEAR: return '';
  2700. default: return '';
  2701. }
  2702. };
  2703. /**
  2704. * @constructor Range
  2705. * A Range controls a numeric range with a start and end value.
  2706. * The Range adjusts the range based on mouse events or programmatic changes,
  2707. * and triggers events when the range is changing or has been changed.
  2708. * @param {RootPanel} root Root panel, used to subscribe to events
  2709. * @param {Panel} parent Parent panel, used to attach to the DOM
  2710. * @param {Object} [options] See description at Range.setOptions
  2711. */
  2712. function Range(root, parent, options) {
  2713. this.id = util.randomUUID();
  2714. this.start = null; // Number
  2715. this.end = null; // Number
  2716. this.root = root;
  2717. this.parent = parent;
  2718. this.options = options || {};
  2719. // drag listeners for dragging
  2720. this.root.on('dragstart', this._onDragStart.bind(this));
  2721. this.root.on('drag', this._onDrag.bind(this));
  2722. this.root.on('dragend', this._onDragEnd.bind(this));
  2723. // ignore dragging when holding
  2724. this.root.on('hold', this._onHold.bind(this));
  2725. // mouse wheel for zooming
  2726. this.root.on('mousewheel', this._onMouseWheel.bind(this));
  2727. this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  2728. // pinch to zoom
  2729. this.root.on('touch', this._onTouch.bind(this));
  2730. this.root.on('pinch', this._onPinch.bind(this));
  2731. this.setOptions(options);
  2732. }
  2733. // turn Range into an event emitter
  2734. Emitter(Range.prototype);
  2735. /**
  2736. * Set options for the range controller
  2737. * @param {Object} options Available options:
  2738. * {Number} min Minimum value for start
  2739. * {Number} max Maximum value for end
  2740. * {Number} zoomMin Set a minimum value for
  2741. * (end - start).
  2742. * {Number} zoomMax Set a maximum value for
  2743. * (end - start).
  2744. */
  2745. Range.prototype.setOptions = function (options) {
  2746. util.extend(this.options, options);
  2747. // re-apply range with new limitations
  2748. if (this.start !== null && this.end !== null) {
  2749. this.setRange(this.start, this.end);
  2750. }
  2751. };
  2752. /**
  2753. * Test whether direction has a valid value
  2754. * @param {String} direction 'horizontal' or 'vertical'
  2755. */
  2756. function validateDirection (direction) {
  2757. if (direction != 'horizontal' && direction != 'vertical') {
  2758. throw new TypeError('Unknown direction "' + direction + '". ' +
  2759. 'Choose "horizontal" or "vertical".');
  2760. }
  2761. }
  2762. /**
  2763. * Set a new start and end range
  2764. * @param {Number} [start]
  2765. * @param {Number} [end]
  2766. */
  2767. Range.prototype.setRange = function(start, end) {
  2768. var changed = this._applyRange(start, end);
  2769. if (changed) {
  2770. var params = {
  2771. start: new Date(this.start),
  2772. end: new Date(this.end)
  2773. };
  2774. this.emit('rangechange', params);
  2775. this.emit('rangechanged', params);
  2776. }
  2777. };
  2778. /**
  2779. * Set a new start and end range. This method is the same as setRange, but
  2780. * does not trigger a range change and range changed event, and it returns
  2781. * true when the range is changed
  2782. * @param {Number} [start]
  2783. * @param {Number} [end]
  2784. * @return {Boolean} changed
  2785. * @private
  2786. */
  2787. Range.prototype._applyRange = function(start, end) {
  2788. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  2789. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  2790. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2791. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2792. diff;
  2793. // check for valid number
  2794. if (isNaN(newStart) || newStart === null) {
  2795. throw new Error('Invalid start "' + start + '"');
  2796. }
  2797. if (isNaN(newEnd) || newEnd === null) {
  2798. throw new Error('Invalid end "' + end + '"');
  2799. }
  2800. // prevent start < end
  2801. if (newEnd < newStart) {
  2802. newEnd = newStart;
  2803. }
  2804. // prevent start < min
  2805. if (min !== null) {
  2806. if (newStart < min) {
  2807. diff = (min - newStart);
  2808. newStart += diff;
  2809. newEnd += diff;
  2810. // prevent end > max
  2811. if (max != null) {
  2812. if (newEnd > max) {
  2813. newEnd = max;
  2814. }
  2815. }
  2816. }
  2817. }
  2818. // prevent end > max
  2819. if (max !== null) {
  2820. if (newEnd > max) {
  2821. diff = (newEnd - max);
  2822. newStart -= diff;
  2823. newEnd -= diff;
  2824. // prevent start < min
  2825. if (min != null) {
  2826. if (newStart < min) {
  2827. newStart = min;
  2828. }
  2829. }
  2830. }
  2831. }
  2832. // prevent (end-start) < zoomMin
  2833. if (this.options.zoomMin !== null) {
  2834. var zoomMin = parseFloat(this.options.zoomMin);
  2835. if (zoomMin < 0) {
  2836. zoomMin = 0;
  2837. }
  2838. if ((newEnd - newStart) < zoomMin) {
  2839. if ((this.end - this.start) === zoomMin) {
  2840. // ignore this action, we are already zoomed to the minimum
  2841. newStart = this.start;
  2842. newEnd = this.end;
  2843. }
  2844. else {
  2845. // zoom to the minimum
  2846. diff = (zoomMin - (newEnd - newStart));
  2847. newStart -= diff / 2;
  2848. newEnd += diff / 2;
  2849. }
  2850. }
  2851. }
  2852. // prevent (end-start) > zoomMax
  2853. if (this.options.zoomMax !== null) {
  2854. var zoomMax = parseFloat(this.options.zoomMax);
  2855. if (zoomMax < 0) {
  2856. zoomMax = 0;
  2857. }
  2858. if ((newEnd - newStart) > zoomMax) {
  2859. if ((this.end - this.start) === zoomMax) {
  2860. // ignore this action, we are already zoomed to the maximum
  2861. newStart = this.start;
  2862. newEnd = this.end;
  2863. }
  2864. else {
  2865. // zoom to the maximum
  2866. diff = ((newEnd - newStart) - zoomMax);
  2867. newStart += diff / 2;
  2868. newEnd -= diff / 2;
  2869. }
  2870. }
  2871. }
  2872. var changed = (this.start != newStart || this.end != newEnd);
  2873. this.start = newStart;
  2874. this.end = newEnd;
  2875. return changed;
  2876. };
  2877. /**
  2878. * Retrieve the current range.
  2879. * @return {Object} An object with start and end properties
  2880. */
  2881. Range.prototype.getRange = function() {
  2882. return {
  2883. start: this.start,
  2884. end: this.end
  2885. };
  2886. };
  2887. /**
  2888. * Calculate the conversion offset and scale for current range, based on
  2889. * the provided width
  2890. * @param {Number} width
  2891. * @returns {{offset: number, scale: number}} conversion
  2892. */
  2893. Range.prototype.conversion = function (width) {
  2894. return Range.conversion(this.start, this.end, width);
  2895. };
  2896. /**
  2897. * Static method to calculate the conversion offset and scale for a range,
  2898. * based on the provided start, end, and width
  2899. * @param {Number} start
  2900. * @param {Number} end
  2901. * @param {Number} width
  2902. * @returns {{offset: number, scale: number}} conversion
  2903. */
  2904. Range.conversion = function (start, end, width) {
  2905. if (width != 0 && (end - start != 0)) {
  2906. return {
  2907. offset: start,
  2908. scale: width / (end - start)
  2909. }
  2910. }
  2911. else {
  2912. return {
  2913. offset: 0,
  2914. scale: 1
  2915. };
  2916. }
  2917. };
  2918. // global (private) object to store drag params
  2919. var touchParams = {};
  2920. /**
  2921. * Start dragging horizontally or vertically
  2922. * @param {Event} event
  2923. * @private
  2924. */
  2925. Range.prototype._onDragStart = function(event) {
  2926. // refuse to drag when we where pinching to prevent the timeline make a jump
  2927. // when releasing the fingers in opposite order from the touch screen
  2928. if (touchParams.ignore) return;
  2929. // TODO: reckon with option movable
  2930. touchParams.start = this.start;
  2931. touchParams.end = this.end;
  2932. var frame = this.parent.frame;
  2933. if (frame) {
  2934. frame.style.cursor = 'move';
  2935. }
  2936. };
  2937. /**
  2938. * Perform dragging operating.
  2939. * @param {Event} event
  2940. * @private
  2941. */
  2942. Range.prototype._onDrag = function (event) {
  2943. var direction = this.options.direction;
  2944. validateDirection(direction);
  2945. // TODO: reckon with option movable
  2946. // refuse to drag when we where pinching to prevent the timeline make a jump
  2947. // when releasing the fingers in opposite order from the touch screen
  2948. if (touchParams.ignore) return;
  2949. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  2950. interval = (touchParams.end - touchParams.start),
  2951. width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
  2952. diffRange = -delta / width * interval;
  2953. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  2954. this.emit('rangechange', {
  2955. start: new Date(this.start),
  2956. end: new Date(this.end)
  2957. });
  2958. };
  2959. /**
  2960. * Stop dragging operating.
  2961. * @param {event} event
  2962. * @private
  2963. */
  2964. Range.prototype._onDragEnd = function (event) {
  2965. // refuse to drag when we where pinching to prevent the timeline make a jump
  2966. // when releasing the fingers in opposite order from the touch screen
  2967. if (touchParams.ignore) return;
  2968. // TODO: reckon with option movable
  2969. if (this.parent.frame) {
  2970. this.parent.frame.style.cursor = 'auto';
  2971. }
  2972. // fire a rangechanged event
  2973. this.emit('rangechanged', {
  2974. start: new Date(this.start),
  2975. end: new Date(this.end)
  2976. });
  2977. };
  2978. /**
  2979. * Event handler for mouse wheel event, used to zoom
  2980. * Code from http://adomas.org/javascript-mouse-wheel/
  2981. * @param {Event} event
  2982. * @private
  2983. */
  2984. Range.prototype._onMouseWheel = function(event) {
  2985. // TODO: reckon with option zoomable
  2986. // retrieve delta
  2987. var delta = 0;
  2988. if (event.wheelDelta) { /* IE/Opera. */
  2989. delta = event.wheelDelta / 120;
  2990. } else if (event.detail) { /* Mozilla case. */
  2991. // In Mozilla, sign of delta is different than in IE.
  2992. // Also, delta is multiple of 3.
  2993. delta = -event.detail / 3;
  2994. }
  2995. // If delta is nonzero, handle it.
  2996. // Basically, delta is now positive if wheel was scrolled up,
  2997. // and negative, if wheel was scrolled down.
  2998. if (delta) {
  2999. // perform the zoom action. Delta is normally 1 or -1
  3000. // adjust a negative delta such that zooming in with delta 0.1
  3001. // equals zooming out with a delta -0.1
  3002. var scale;
  3003. if (delta < 0) {
  3004. scale = 1 - (delta / 5);
  3005. }
  3006. else {
  3007. scale = 1 / (1 + (delta / 5)) ;
  3008. }
  3009. // calculate center, the date to zoom around
  3010. var gesture = util.fakeGesture(this, event),
  3011. pointer = getPointer(gesture.center, this.parent.frame),
  3012. pointerDate = this._pointerToDate(pointer);
  3013. this.zoom(scale, pointerDate);
  3014. }
  3015. // Prevent default actions caused by mouse wheel
  3016. // (else the page and timeline both zoom and scroll)
  3017. event.preventDefault();
  3018. };
  3019. /**
  3020. * Start of a touch gesture
  3021. * @private
  3022. */
  3023. Range.prototype._onTouch = function (event) {
  3024. touchParams.start = this.start;
  3025. touchParams.end = this.end;
  3026. touchParams.ignore = false;
  3027. touchParams.center = null;
  3028. // don't move the range when dragging a selected event
  3029. // TODO: it's not so neat to have to know about the state of the ItemSet
  3030. var item = ItemSet.itemFromTarget(event);
  3031. if (item && item.selected && this.options.editable) {
  3032. touchParams.ignore = true;
  3033. }
  3034. };
  3035. /**
  3036. * On start of a hold gesture
  3037. * @private
  3038. */
  3039. Range.prototype._onHold = function () {
  3040. touchParams.ignore = true;
  3041. };
  3042. /**
  3043. * Handle pinch event
  3044. * @param {Event} event
  3045. * @private
  3046. */
  3047. Range.prototype._onPinch = function (event) {
  3048. var direction = this.options.direction;
  3049. touchParams.ignore = true;
  3050. // TODO: reckon with option zoomable
  3051. if (event.gesture.touches.length > 1) {
  3052. if (!touchParams.center) {
  3053. touchParams.center = getPointer(event.gesture.center, this.parent.frame);
  3054. }
  3055. var scale = 1 / event.gesture.scale,
  3056. initDate = this._pointerToDate(touchParams.center),
  3057. center = getPointer(event.gesture.center, this.parent.frame),
  3058. date = this._pointerToDate(this.parent, center),
  3059. delta = date - initDate; // TODO: utilize delta
  3060. // calculate new start and end
  3061. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3062. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3063. // apply new range
  3064. this.setRange(newStart, newEnd);
  3065. }
  3066. };
  3067. /**
  3068. * Helper function to calculate the center date for zooming
  3069. * @param {{x: Number, y: Number}} pointer
  3070. * @return {number} date
  3071. * @private
  3072. */
  3073. Range.prototype._pointerToDate = function (pointer) {
  3074. var conversion;
  3075. var direction = this.options.direction;
  3076. validateDirection(direction);
  3077. if (direction == 'horizontal') {
  3078. var width = this.parent.width;
  3079. conversion = this.conversion(width);
  3080. return pointer.x / conversion.scale + conversion.offset;
  3081. }
  3082. else {
  3083. var height = this.parent.height;
  3084. conversion = this.conversion(height);
  3085. return pointer.y / conversion.scale + conversion.offset;
  3086. }
  3087. };
  3088. /**
  3089. * Get the pointer location relative to the location of the dom element
  3090. * @param {{pageX: Number, pageY: Number}} touch
  3091. * @param {Element} element HTML DOM element
  3092. * @return {{x: Number, y: Number}} pointer
  3093. * @private
  3094. */
  3095. function getPointer (touch, element) {
  3096. return {
  3097. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3098. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3099. };
  3100. }
  3101. /**
  3102. * Zoom the range the given scale in or out. Start and end date will
  3103. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3104. * date around which to zoom.
  3105. * For example, try scale = 0.9 or 1.1
  3106. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3107. * values below 1 will zoom in.
  3108. * @param {Number} [center] Value representing a date around which will
  3109. * be zoomed.
  3110. */
  3111. Range.prototype.zoom = function(scale, center) {
  3112. // if centerDate is not provided, take it half between start Date and end Date
  3113. if (center == null) {
  3114. center = (this.start + this.end) / 2;
  3115. }
  3116. // calculate new start and end
  3117. var newStart = center + (this.start - center) * scale;
  3118. var newEnd = center + (this.end - center) * scale;
  3119. this.setRange(newStart, newEnd);
  3120. };
  3121. /**
  3122. * Move the range with a given delta to the left or right. Start and end
  3123. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3124. * @param {Number} delta Moving amount. Positive value will move right,
  3125. * negative value will move left
  3126. */
  3127. Range.prototype.move = function(delta) {
  3128. // zoom start Date and end Date relative to the centerDate
  3129. var diff = (this.end - this.start);
  3130. // apply new values
  3131. var newStart = this.start + diff * delta;
  3132. var newEnd = this.end + diff * delta;
  3133. // TODO: reckon with min and max range
  3134. this.start = newStart;
  3135. this.end = newEnd;
  3136. };
  3137. /**
  3138. * Move the range to a new center point
  3139. * @param {Number} moveTo New center point of the range
  3140. */
  3141. Range.prototype.moveTo = function(moveTo) {
  3142. var center = (this.start + this.end) / 2;
  3143. var diff = center - moveTo;
  3144. // calculate new start and end
  3145. var newStart = this.start - diff;
  3146. var newEnd = this.end - diff;
  3147. this.setRange(newStart, newEnd);
  3148. };
  3149. /**
  3150. * Prototype for visual components
  3151. */
  3152. function Component () {
  3153. this.id = null;
  3154. this.parent = null;
  3155. this.childs = null;
  3156. this.options = null;
  3157. this.top = 0;
  3158. this.left = 0;
  3159. this.width = 0;
  3160. this.height = 0;
  3161. }
  3162. // Turn the Component into an event emitter
  3163. Emitter(Component.prototype);
  3164. /**
  3165. * Set parameters for the frame. Parameters will be merged in current parameter
  3166. * set.
  3167. * @param {Object} options Available parameters:
  3168. * {String | function} [className]
  3169. * {String | Number | function} [left]
  3170. * {String | Number | function} [top]
  3171. * {String | Number | function} [width]
  3172. * {String | Number | function} [height]
  3173. */
  3174. Component.prototype.setOptions = function setOptions(options) {
  3175. if (options) {
  3176. util.extend(this.options, options);
  3177. this.repaint();
  3178. }
  3179. };
  3180. /**
  3181. * Get an option value by name
  3182. * The function will first check this.options object, and else will check
  3183. * this.defaultOptions.
  3184. * @param {String} name
  3185. * @return {*} value
  3186. */
  3187. Component.prototype.getOption = function getOption(name) {
  3188. var value;
  3189. if (this.options) {
  3190. value = this.options[name];
  3191. }
  3192. if (value === undefined && this.defaultOptions) {
  3193. value = this.defaultOptions[name];
  3194. }
  3195. return value;
  3196. };
  3197. /**
  3198. * Get the frame element of the component, the outer HTML DOM element.
  3199. * @returns {HTMLElement | null} frame
  3200. */
  3201. Component.prototype.getFrame = function getFrame() {
  3202. // should be implemented by the component
  3203. return null;
  3204. };
  3205. /**
  3206. * Repaint the component
  3207. * @return {boolean} Returns true if the component is resized
  3208. */
  3209. Component.prototype.repaint = function repaint() {
  3210. // should be implemented by the component
  3211. return false;
  3212. };
  3213. /**
  3214. * Test whether the component is resized since the last time _isResized() was
  3215. * called.
  3216. * @return {Boolean} Returns true if the component is resized
  3217. * @protected
  3218. */
  3219. Component.prototype._isResized = function _isResized() {
  3220. var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
  3221. this._previousWidth = this.width;
  3222. this._previousHeight = this.height;
  3223. return resized;
  3224. };
  3225. /**
  3226. * A panel can contain components
  3227. * @param {Object} [options] Available parameters:
  3228. * {String | Number | function} [left]
  3229. * {String | Number | function} [top]
  3230. * {String | Number | function} [width]
  3231. * {String | Number | function} [height]
  3232. * {String | function} [className]
  3233. * @constructor Panel
  3234. * @extends Component
  3235. */
  3236. function Panel(options) {
  3237. this.id = util.randomUUID();
  3238. this.parent = null;
  3239. this.childs = [];
  3240. this.options = options || {};
  3241. // create frame
  3242. this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
  3243. }
  3244. Panel.prototype = new Component();
  3245. /**
  3246. * Set options. Will extend the current options.
  3247. * @param {Object} [options] Available parameters:
  3248. * {String | function} [className]
  3249. * {String | Number | function} [left]
  3250. * {String | Number | function} [top]
  3251. * {String | Number | function} [width]
  3252. * {String | Number | function} [height]
  3253. */
  3254. Panel.prototype.setOptions = Component.prototype.setOptions;
  3255. /**
  3256. * Get the outer frame of the panel
  3257. * @returns {HTMLElement} frame
  3258. */
  3259. Panel.prototype.getFrame = function () {
  3260. return this.frame;
  3261. };
  3262. /**
  3263. * Append a child to the panel
  3264. * @param {Component} child
  3265. */
  3266. Panel.prototype.appendChild = function (child) {
  3267. this.childs.push(child);
  3268. child.parent = this;
  3269. // attach to the DOM
  3270. var frame = child.getFrame();
  3271. if (frame) {
  3272. if (frame.parentNode) {
  3273. frame.parentNode.removeChild(frame);
  3274. }
  3275. this.frame.appendChild(frame);
  3276. }
  3277. };
  3278. /**
  3279. * Insert a child to the panel
  3280. * @param {Component} child
  3281. * @param {Component} beforeChild
  3282. */
  3283. Panel.prototype.insertBefore = function (child, beforeChild) {
  3284. var index = this.childs.indexOf(beforeChild);
  3285. if (index != -1) {
  3286. this.childs.splice(index, 0, child);
  3287. child.parent = this;
  3288. // attach to the DOM
  3289. var frame = child.getFrame();
  3290. if (frame) {
  3291. if (frame.parentNode) {
  3292. frame.parentNode.removeChild(frame);
  3293. }
  3294. var beforeFrame = beforeChild.getFrame();
  3295. if (beforeFrame) {
  3296. this.frame.insertBefore(frame, beforeFrame);
  3297. }
  3298. else {
  3299. this.frame.appendChild(frame);
  3300. }
  3301. }
  3302. }
  3303. };
  3304. /**
  3305. * Remove a child from the panel
  3306. * @param {Component} child
  3307. */
  3308. Panel.prototype.removeChild = function (child) {
  3309. var index = this.childs.indexOf(child);
  3310. if (index != -1) {
  3311. this.childs.splice(index, 1);
  3312. child.parent = null;
  3313. // remove from the DOM
  3314. var frame = child.getFrame();
  3315. if (frame && frame.parentNode) {
  3316. this.frame.removeChild(frame);
  3317. }
  3318. }
  3319. };
  3320. /**
  3321. * Test whether the panel contains given child
  3322. * @param {Component} child
  3323. */
  3324. Panel.prototype.hasChild = function (child) {
  3325. var index = this.childs.indexOf(child);
  3326. return (index != -1);
  3327. };
  3328. /**
  3329. * Repaint the component
  3330. * @return {boolean} Returns true if the component was resized since previous repaint
  3331. */
  3332. Panel.prototype.repaint = function () {
  3333. var asString = util.option.asString,
  3334. options = this.options,
  3335. frame = this.getFrame();
  3336. // update className
  3337. frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
  3338. // repaint the child components
  3339. var childsResized = this._repaintChilds();
  3340. // update frame size
  3341. this._updateSize();
  3342. return this._isResized() || childsResized;
  3343. };
  3344. /**
  3345. * Repaint all childs of the panel
  3346. * @return {boolean} Returns true if the component is resized
  3347. * @private
  3348. */
  3349. Panel.prototype._repaintChilds = function () {
  3350. var resized = false;
  3351. for (var i = 0, ii = this.childs.length; i < ii; i++) {
  3352. resized = this.childs[i].repaint() || resized;
  3353. }
  3354. return resized;
  3355. };
  3356. /**
  3357. * Apply the size from options to the panel, and recalculate it's actual size.
  3358. * @private
  3359. */
  3360. Panel.prototype._updateSize = function () {
  3361. // apply size
  3362. this.frame.style.top = util.option.asSize(this.options.top);
  3363. this.frame.style.bottom = util.option.asSize(this.options.bottom);
  3364. this.frame.style.left = util.option.asSize(this.options.left);
  3365. this.frame.style.right = util.option.asSize(this.options.right);
  3366. this.frame.style.width = util.option.asSize(this.options.width, '100%');
  3367. this.frame.style.height = util.option.asSize(this.options.height, '');
  3368. // get actual size
  3369. this.top = this.frame.offsetTop;
  3370. this.left = this.frame.offsetLeft;
  3371. this.width = this.frame.offsetWidth;
  3372. this.height = this.frame.offsetHeight;
  3373. };
  3374. /**
  3375. * A root panel can hold components. The root panel must be initialized with
  3376. * a DOM element as container.
  3377. * @param {HTMLElement} container
  3378. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3379. * @constructor RootPanel
  3380. * @extends Panel
  3381. */
  3382. function RootPanel(container, options) {
  3383. this.id = util.randomUUID();
  3384. this.container = container;
  3385. this.options = options || {};
  3386. this.defaultOptions = {
  3387. autoResize: true
  3388. };
  3389. // create the HTML DOM
  3390. this._create();
  3391. // attach the root panel to the provided container
  3392. if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
  3393. this.container.appendChild(this.getFrame());
  3394. this._initWatch();
  3395. }
  3396. RootPanel.prototype = new Panel();
  3397. /**
  3398. * Create the HTML DOM for the root panel
  3399. */
  3400. RootPanel.prototype._create = function _create() {
  3401. // create frame
  3402. this.frame = document.createElement('div');
  3403. // create event listeners for all interesting events, these events will be
  3404. // emitted via emitter
  3405. this.hammer = Hammer(this.frame, {
  3406. prevent_default: true
  3407. });
  3408. this.listeners = {};
  3409. var me = this;
  3410. var events = [
  3411. 'touch', 'pinch', 'tap', 'doubletap', 'hold',
  3412. 'dragstart', 'drag', 'dragend',
  3413. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
  3414. ];
  3415. events.forEach(function (event) {
  3416. var listener = function () {
  3417. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  3418. me.emit.apply(me, args);
  3419. };
  3420. me.hammer.on(event, listener);
  3421. me.listeners[event] = listener;
  3422. });
  3423. };
  3424. /**
  3425. * Set options. Will extend the current options.
  3426. * @param {Object} [options] Available parameters:
  3427. * {String | function} [className]
  3428. * {String | Number | function} [left]
  3429. * {String | Number | function} [top]
  3430. * {String | Number | function} [width]
  3431. * {String | Number | function} [height]
  3432. * {Boolean | function} [autoResize]
  3433. */
  3434. RootPanel.prototype.setOptions = function setOptions(options) {
  3435. if (options) {
  3436. util.extend(this.options, options);
  3437. this.repaint();
  3438. this._initWatch();
  3439. }
  3440. };
  3441. /**
  3442. * Get the frame of the root panel
  3443. */
  3444. RootPanel.prototype.getFrame = function getFrame() {
  3445. return this.frame;
  3446. };
  3447. /**
  3448. * Repaint the root panel
  3449. */
  3450. RootPanel.prototype.repaint = function repaint() {
  3451. // update class name
  3452. var options = this.options;
  3453. var editable = options.editable.updateTime || options.editable.updateGroup;
  3454. var className = 'vis timeline rootpanel ' + options.orientation + (editable ? ' editable' : '');
  3455. if (options.className) className += ' ' + util.option.asString(className);
  3456. this.frame.className = className;
  3457. // repaint the child components
  3458. var childsResized = this._repaintChilds();
  3459. // update frame size
  3460. this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
  3461. this._updateSize();
  3462. // if the root panel or any of its childs is resized, repaint again,
  3463. // as other components may need to be resized accordingly
  3464. var resized = this._isResized() || childsResized;
  3465. if (resized) {
  3466. setTimeout(this.repaint.bind(this), 0);
  3467. }
  3468. };
  3469. /**
  3470. * Initialize watching when option autoResize is true
  3471. * @private
  3472. */
  3473. RootPanel.prototype._initWatch = function _initWatch() {
  3474. var autoResize = this.getOption('autoResize');
  3475. if (autoResize) {
  3476. this._watch();
  3477. }
  3478. else {
  3479. this._unwatch();
  3480. }
  3481. };
  3482. /**
  3483. * Watch for changes in the size of the frame. On resize, the Panel will
  3484. * automatically redraw itself.
  3485. * @private
  3486. */
  3487. RootPanel.prototype._watch = function _watch() {
  3488. var me = this;
  3489. this._unwatch();
  3490. var checkSize = function checkSize() {
  3491. var autoResize = me.getOption('autoResize');
  3492. if (!autoResize) {
  3493. // stop watching when the option autoResize is changed to false
  3494. me._unwatch();
  3495. return;
  3496. }
  3497. if (me.frame) {
  3498. // check whether the frame is resized
  3499. if ((me.frame.clientWidth != me.lastWidth) ||
  3500. (me.frame.clientHeight != me.lastHeight)) {
  3501. me.lastWidth = me.frame.clientWidth;
  3502. me.lastHeight = me.frame.clientHeight;
  3503. me.repaint();
  3504. // TODO: emit a resize event instead?
  3505. }
  3506. }
  3507. };
  3508. // TODO: automatically cleanup the event listener when the frame is deleted
  3509. util.addEventListener(window, 'resize', checkSize);
  3510. this.watchTimer = setInterval(checkSize, 1000);
  3511. };
  3512. /**
  3513. * Stop watching for a resize of the frame.
  3514. * @private
  3515. */
  3516. RootPanel.prototype._unwatch = function _unwatch() {
  3517. if (this.watchTimer) {
  3518. clearInterval(this.watchTimer);
  3519. this.watchTimer = undefined;
  3520. }
  3521. // TODO: remove event listener on window.resize
  3522. };
  3523. /**
  3524. * A horizontal time axis
  3525. * @param {Object} [options] See TimeAxis.setOptions for the available
  3526. * options.
  3527. * @constructor TimeAxis
  3528. * @extends Component
  3529. */
  3530. function TimeAxis (options) {
  3531. this.id = util.randomUUID();
  3532. this.dom = {
  3533. majorLines: [],
  3534. majorTexts: [],
  3535. minorLines: [],
  3536. minorTexts: [],
  3537. redundant: {
  3538. majorLines: [],
  3539. majorTexts: [],
  3540. minorLines: [],
  3541. minorTexts: []
  3542. }
  3543. };
  3544. this.props = {
  3545. range: {
  3546. start: 0,
  3547. end: 0,
  3548. minimumStep: 0
  3549. },
  3550. lineTop: 0
  3551. };
  3552. this.options = options || {};
  3553. this.defaultOptions = {
  3554. orientation: 'bottom', // supported: 'top', 'bottom'
  3555. // TODO: implement timeaxis orientations 'left' and 'right'
  3556. showMinorLabels: true,
  3557. showMajorLabels: true
  3558. };
  3559. this.range = null;
  3560. // create the HTML DOM
  3561. this._create();
  3562. }
  3563. TimeAxis.prototype = new Component();
  3564. // TODO: comment options
  3565. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3566. /**
  3567. * Create the HTML DOM for the TimeAxis
  3568. */
  3569. TimeAxis.prototype._create = function _create() {
  3570. this.frame = document.createElement('div');
  3571. };
  3572. /**
  3573. * Set a range (start and end)
  3574. * @param {Range | Object} range A Range or an object containing start and end.
  3575. */
  3576. TimeAxis.prototype.setRange = function (range) {
  3577. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3578. throw new TypeError('Range must be an instance of Range, ' +
  3579. 'or an object containing start and end.');
  3580. }
  3581. this.range = range;
  3582. };
  3583. /**
  3584. * Get the outer frame of the time axis
  3585. * @return {HTMLElement} frame
  3586. */
  3587. TimeAxis.prototype.getFrame = function getFrame() {
  3588. return this.frame;
  3589. };
  3590. /**
  3591. * Repaint the component
  3592. * @return {boolean} Returns true if the component is resized
  3593. */
  3594. TimeAxis.prototype.repaint = function () {
  3595. var asSize = util.option.asSize,
  3596. options = this.options,
  3597. props = this.props,
  3598. frame = this.frame;
  3599. // update classname
  3600. frame.className = 'timeaxis'; // TODO: add className from options if defined
  3601. var parent = frame.parentNode;
  3602. if (parent) {
  3603. // calculate character width and height
  3604. this._calculateCharSize();
  3605. // TODO: recalculate sizes only needed when parent is resized or options is changed
  3606. var orientation = this.getOption('orientation'),
  3607. showMinorLabels = this.getOption('showMinorLabels'),
  3608. showMajorLabels = this.getOption('showMajorLabels');
  3609. // determine the width and height of the elemens for the axis
  3610. var parentHeight = this.parent.height;
  3611. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3612. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3613. this.height = props.minorLabelHeight + props.majorLabelHeight;
  3614. this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
  3615. props.minorLineHeight = parentHeight + props.minorLabelHeight;
  3616. props.minorLineWidth = 1; // TODO: really calculate width
  3617. props.majorLineHeight = parentHeight + this.height;
  3618. props.majorLineWidth = 1; // TODO: really calculate width
  3619. // take frame offline while updating (is almost twice as fast)
  3620. var beforeChild = frame.nextSibling;
  3621. parent.removeChild(frame);
  3622. // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
  3623. if (orientation == 'top') {
  3624. frame.style.top = '0';
  3625. frame.style.left = '0';
  3626. frame.style.bottom = '';
  3627. frame.style.width = asSize(options.width, '100%');
  3628. frame.style.height = this.height + 'px';
  3629. }
  3630. else { // bottom
  3631. frame.style.top = '';
  3632. frame.style.bottom = '0';
  3633. frame.style.left = '0';
  3634. frame.style.width = asSize(options.width, '100%');
  3635. frame.style.height = this.height + 'px';
  3636. }
  3637. this._repaintLabels();
  3638. this._repaintLine();
  3639. // put frame online again
  3640. if (beforeChild) {
  3641. parent.insertBefore(frame, beforeChild);
  3642. }
  3643. else {
  3644. parent.appendChild(frame)
  3645. }
  3646. }
  3647. return this._isResized();
  3648. };
  3649. /**
  3650. * Repaint major and minor text labels and vertical grid lines
  3651. * @private
  3652. */
  3653. TimeAxis.prototype._repaintLabels = function () {
  3654. var orientation = this.getOption('orientation');
  3655. // calculate range and step (step such that we have space for 7 characters per label)
  3656. var start = util.convert(this.range.start, 'Number'),
  3657. end = util.convert(this.range.end, 'Number'),
  3658. minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
  3659. -this.options.toTime(0).valueOf();
  3660. var step = new TimeStep(new Date(start), new Date(end), minimumStep);
  3661. this.step = step;
  3662. // Move all DOM elements to a "redundant" list, where they
  3663. // can be picked for re-use, and clear the lists with lines and texts.
  3664. // At the end of the function _repaintLabels, left over elements will be cleaned up
  3665. var dom = this.dom;
  3666. dom.redundant.majorLines = dom.majorLines;
  3667. dom.redundant.majorTexts = dom.majorTexts;
  3668. dom.redundant.minorLines = dom.minorLines;
  3669. dom.redundant.minorTexts = dom.minorTexts;
  3670. dom.majorLines = [];
  3671. dom.majorTexts = [];
  3672. dom.minorLines = [];
  3673. dom.minorTexts = [];
  3674. step.first();
  3675. var xFirstMajorLabel = undefined;
  3676. var max = 0;
  3677. while (step.hasNext() && max < 1000) {
  3678. max++;
  3679. var cur = step.getCurrent(),
  3680. x = this.options.toScreen(cur),
  3681. isMajor = step.isMajor();
  3682. // TODO: lines must have a width, such that we can create css backgrounds
  3683. if (this.getOption('showMinorLabels')) {
  3684. this._repaintMinorText(x, step.getLabelMinor(), orientation);
  3685. }
  3686. if (isMajor && this.getOption('showMajorLabels')) {
  3687. if (x > 0) {
  3688. if (xFirstMajorLabel == undefined) {
  3689. xFirstMajorLabel = x;
  3690. }
  3691. this._repaintMajorText(x, step.getLabelMajor(), orientation);
  3692. }
  3693. this._repaintMajorLine(x, orientation);
  3694. }
  3695. else {
  3696. this._repaintMinorLine(x, orientation);
  3697. }
  3698. step.next();
  3699. }
  3700. // create a major label on the left when needed
  3701. if (this.getOption('showMajorLabels')) {
  3702. var leftTime = this.options.toTime(0),
  3703. leftText = step.getLabelMajor(leftTime),
  3704. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  3705. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3706. this._repaintMajorText(0, leftText, orientation);
  3707. }
  3708. }
  3709. // Cleanup leftover DOM elements from the redundant list
  3710. util.forEach(this.dom.redundant, function (arr) {
  3711. while (arr.length) {
  3712. var elem = arr.pop();
  3713. if (elem && elem.parentNode) {
  3714. elem.parentNode.removeChild(elem);
  3715. }
  3716. }
  3717. });
  3718. };
  3719. /**
  3720. * Create a minor label for the axis at position x
  3721. * @param {Number} x
  3722. * @param {String} text
  3723. * @param {String} orientation "top" or "bottom" (default)
  3724. * @private
  3725. */
  3726. TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
  3727. // reuse redundant label
  3728. var label = this.dom.redundant.minorTexts.shift();
  3729. if (!label) {
  3730. // create new label
  3731. var content = document.createTextNode('');
  3732. label = document.createElement('div');
  3733. label.appendChild(content);
  3734. label.className = 'text minor';
  3735. this.frame.appendChild(label);
  3736. }
  3737. this.dom.minorTexts.push(label);
  3738. label.childNodes[0].nodeValue = text;
  3739. if (orientation == 'top') {
  3740. label.style.top = this.props.majorLabelHeight + 'px';
  3741. label.style.bottom = '';
  3742. }
  3743. else {
  3744. label.style.top = '';
  3745. label.style.bottom = this.props.majorLabelHeight + 'px';
  3746. }
  3747. label.style.left = x + 'px';
  3748. //label.title = title; // TODO: this is a heavy operation
  3749. };
  3750. /**
  3751. * Create a Major label for the axis at position x
  3752. * @param {Number} x
  3753. * @param {String} text
  3754. * @param {String} orientation "top" or "bottom" (default)
  3755. * @private
  3756. */
  3757. TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
  3758. // reuse redundant label
  3759. var label = this.dom.redundant.majorTexts.shift();
  3760. if (!label) {
  3761. // create label
  3762. var content = document.createTextNode(text);
  3763. label = document.createElement('div');
  3764. label.className = 'text major';
  3765. label.appendChild(content);
  3766. this.frame.appendChild(label);
  3767. }
  3768. this.dom.majorTexts.push(label);
  3769. label.childNodes[0].nodeValue = text;
  3770. //label.title = title; // TODO: this is a heavy operation
  3771. if (orientation == 'top') {
  3772. label.style.top = '0px';
  3773. label.style.bottom = '';
  3774. }
  3775. else {
  3776. label.style.top = '';
  3777. label.style.bottom = '0px';
  3778. }
  3779. label.style.left = x + 'px';
  3780. };
  3781. /**
  3782. * Create a minor line for the axis at position x
  3783. * @param {Number} x
  3784. * @param {String} orientation "top" or "bottom" (default)
  3785. * @private
  3786. */
  3787. TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
  3788. // reuse redundant line
  3789. var line = this.dom.redundant.minorLines.shift();
  3790. if (!line) {
  3791. // create vertical line
  3792. line = document.createElement('div');
  3793. line.className = 'grid vertical minor';
  3794. this.frame.appendChild(line);
  3795. }
  3796. this.dom.minorLines.push(line);
  3797. var props = this.props;
  3798. if (orientation == 'top') {
  3799. line.style.top = this.props.majorLabelHeight + 'px';
  3800. line.style.bottom = '';
  3801. }
  3802. else {
  3803. line.style.top = '';
  3804. line.style.bottom = this.props.majorLabelHeight + 'px';
  3805. }
  3806. line.style.height = props.minorLineHeight + 'px';
  3807. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3808. };
  3809. /**
  3810. * Create a Major line for the axis at position x
  3811. * @param {Number} x
  3812. * @param {String} orientation "top" or "bottom" (default)
  3813. * @private
  3814. */
  3815. TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
  3816. // reuse redundant line
  3817. var line = this.dom.redundant.majorLines.shift();
  3818. if (!line) {
  3819. // create vertical line
  3820. line = document.createElement('DIV');
  3821. line.className = 'grid vertical major';
  3822. this.frame.appendChild(line);
  3823. }
  3824. this.dom.majorLines.push(line);
  3825. var props = this.props;
  3826. if (orientation == 'top') {
  3827. line.style.top = '0px';
  3828. line.style.bottom = '';
  3829. }
  3830. else {
  3831. line.style.top = '';
  3832. line.style.bottom = '0px';
  3833. }
  3834. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3835. line.style.height = props.majorLineHeight + 'px';
  3836. };
  3837. /**
  3838. * Repaint the horizontal line for the axis
  3839. * @private
  3840. */
  3841. TimeAxis.prototype._repaintLine = function() {
  3842. var line = this.dom.line,
  3843. frame = this.frame,
  3844. orientation = this.getOption('orientation');
  3845. // line before all axis elements
  3846. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  3847. if (line) {
  3848. // put this line at the end of all childs
  3849. frame.removeChild(line);
  3850. frame.appendChild(line);
  3851. }
  3852. else {
  3853. // create the axis line
  3854. line = document.createElement('div');
  3855. line.className = 'grid horizontal major';
  3856. frame.appendChild(line);
  3857. this.dom.line = line;
  3858. }
  3859. if (orientation == 'top') {
  3860. line.style.top = this.height + 'px';
  3861. line.style.bottom = '';
  3862. }
  3863. else {
  3864. line.style.top = '';
  3865. line.style.bottom = this.height + 'px';
  3866. }
  3867. }
  3868. else {
  3869. if (line && line.parentNode) {
  3870. line.parentNode.removeChild(line);
  3871. delete this.dom.line;
  3872. }
  3873. }
  3874. };
  3875. /**
  3876. * Determine the size of text on the axis (both major and minor axis).
  3877. * The size is calculated only once and then cached in this.props.
  3878. * @private
  3879. */
  3880. TimeAxis.prototype._calculateCharSize = function () {
  3881. // determine the char width and height on the minor axis
  3882. if (!('minorCharHeight' in this.props)) {
  3883. var textMinor = document.createTextNode('0');
  3884. var measureCharMinor = document.createElement('DIV');
  3885. measureCharMinor.className = 'text minor measure';
  3886. measureCharMinor.appendChild(textMinor);
  3887. this.frame.appendChild(measureCharMinor);
  3888. this.props.minorCharHeight = measureCharMinor.clientHeight;
  3889. this.props.minorCharWidth = measureCharMinor.clientWidth;
  3890. this.frame.removeChild(measureCharMinor);
  3891. }
  3892. if (!('majorCharHeight' in this.props)) {
  3893. var textMajor = document.createTextNode('0');
  3894. var measureCharMajor = document.createElement('DIV');
  3895. measureCharMajor.className = 'text major measure';
  3896. measureCharMajor.appendChild(textMajor);
  3897. this.frame.appendChild(measureCharMajor);
  3898. this.props.majorCharHeight = measureCharMajor.clientHeight;
  3899. this.props.majorCharWidth = measureCharMajor.clientWidth;
  3900. this.frame.removeChild(measureCharMajor);
  3901. }
  3902. };
  3903. /**
  3904. * Snap a date to a rounded value.
  3905. * The snap intervals are dependent on the current scale and step.
  3906. * @param {Date} date the date to be snapped.
  3907. * @return {Date} snappedDate
  3908. */
  3909. TimeAxis.prototype.snap = function snap (date) {
  3910. return this.step.snap(date);
  3911. };
  3912. /**
  3913. * A current time bar
  3914. * @param {Range} range
  3915. * @param {Object} [options] Available parameters:
  3916. * {Boolean} [showCurrentTime]
  3917. * @constructor CurrentTime
  3918. * @extends Component
  3919. */
  3920. function CurrentTime (range, options) {
  3921. this.id = util.randomUUID();
  3922. this.range = range;
  3923. this.options = options || {};
  3924. this.defaultOptions = {
  3925. showCurrentTime: false
  3926. };
  3927. this._create();
  3928. }
  3929. CurrentTime.prototype = new Component();
  3930. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  3931. /**
  3932. * Create the HTML DOM for the current time bar
  3933. * @private
  3934. */
  3935. CurrentTime.prototype._create = function _create () {
  3936. var bar = document.createElement('div');
  3937. bar.className = 'currenttime';
  3938. bar.style.position = 'absolute';
  3939. bar.style.top = '0px';
  3940. bar.style.height = '100%';
  3941. this.bar = bar;
  3942. };
  3943. /**
  3944. * Get the frame element of the current time bar
  3945. * @returns {HTMLElement} frame
  3946. */
  3947. CurrentTime.prototype.getFrame = function getFrame() {
  3948. return this.bar;
  3949. };
  3950. /**
  3951. * Repaint the component
  3952. * @return {boolean} Returns true if the component is resized
  3953. */
  3954. CurrentTime.prototype.repaint = function repaint() {
  3955. var parent = this.parent;
  3956. var now = new Date();
  3957. var x = this.options.toScreen(now);
  3958. this.bar.style.left = x + 'px';
  3959. this.bar.title = 'Current time: ' + now;
  3960. return false;
  3961. };
  3962. /**
  3963. * Start auto refreshing the current time bar
  3964. */
  3965. CurrentTime.prototype.start = function start() {
  3966. var me = this;
  3967. function update () {
  3968. me.stop();
  3969. // determine interval to refresh
  3970. var scale = me.range.conversion(me.parent.width).scale;
  3971. var interval = 1 / scale / 10;
  3972. if (interval < 30) interval = 30;
  3973. if (interval > 1000) interval = 1000;
  3974. me.repaint();
  3975. // start a timer to adjust for the new time
  3976. me.currentTimeTimer = setTimeout(update, interval);
  3977. }
  3978. update();
  3979. };
  3980. /**
  3981. * Stop auto refreshing the current time bar
  3982. */
  3983. CurrentTime.prototype.stop = function stop() {
  3984. if (this.currentTimeTimer !== undefined) {
  3985. clearTimeout(this.currentTimeTimer);
  3986. delete this.currentTimeTimer;
  3987. }
  3988. };
  3989. /**
  3990. * A custom time bar
  3991. * @param {Object} [options] Available parameters:
  3992. * {Boolean} [showCustomTime]
  3993. * @constructor CustomTime
  3994. * @extends Component
  3995. */
  3996. function CustomTime (options) {
  3997. this.id = util.randomUUID();
  3998. this.options = options || {};
  3999. this.defaultOptions = {
  4000. showCustomTime: false
  4001. };
  4002. this.customTime = new Date();
  4003. this.eventParams = {}; // stores state parameters while dragging the bar
  4004. // create the DOM
  4005. this._create();
  4006. }
  4007. CustomTime.prototype = new Component();
  4008. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4009. /**
  4010. * Create the DOM for the custom time
  4011. * @private
  4012. */
  4013. CustomTime.prototype._create = function _create () {
  4014. var bar = document.createElement('div');
  4015. bar.className = 'customtime';
  4016. bar.style.position = 'absolute';
  4017. bar.style.top = '0px';
  4018. bar.style.height = '100%';
  4019. this.bar = bar;
  4020. var drag = document.createElement('div');
  4021. drag.style.position = 'relative';
  4022. drag.style.top = '0px';
  4023. drag.style.left = '-10px';
  4024. drag.style.height = '100%';
  4025. drag.style.width = '20px';
  4026. bar.appendChild(drag);
  4027. // attach event listeners
  4028. this.hammer = Hammer(bar, {
  4029. prevent_default: true
  4030. });
  4031. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4032. this.hammer.on('drag', this._onDrag.bind(this));
  4033. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4034. };
  4035. /**
  4036. * Get the frame element of the custom time bar
  4037. * @returns {HTMLElement} frame
  4038. */
  4039. CustomTime.prototype.getFrame = function getFrame() {
  4040. return this.bar;
  4041. };
  4042. /**
  4043. * Repaint the component
  4044. * @return {boolean} Returns true if the component is resized
  4045. */
  4046. CustomTime.prototype.repaint = function () {
  4047. var x = this.options.toScreen(this.customTime);
  4048. this.bar.style.left = x + 'px';
  4049. this.bar.title = 'Time: ' + this.customTime;
  4050. return false;
  4051. };
  4052. /**
  4053. * Set custom time.
  4054. * @param {Date} time
  4055. */
  4056. CustomTime.prototype.setCustomTime = function(time) {
  4057. this.customTime = new Date(time.valueOf());
  4058. this.repaint();
  4059. };
  4060. /**
  4061. * Retrieve the current custom time.
  4062. * @return {Date} customTime
  4063. */
  4064. CustomTime.prototype.getCustomTime = function() {
  4065. return new Date(this.customTime.valueOf());
  4066. };
  4067. /**
  4068. * Start moving horizontally
  4069. * @param {Event} event
  4070. * @private
  4071. */
  4072. CustomTime.prototype._onDragStart = function(event) {
  4073. this.eventParams.dragging = true;
  4074. this.eventParams.customTime = this.customTime;
  4075. event.stopPropagation();
  4076. event.preventDefault();
  4077. };
  4078. /**
  4079. * Perform moving operating.
  4080. * @param {Event} event
  4081. * @private
  4082. */
  4083. CustomTime.prototype._onDrag = function (event) {
  4084. if (!this.eventParams.dragging) return;
  4085. var deltaX = event.gesture.deltaX,
  4086. x = this.options.toScreen(this.eventParams.customTime) + deltaX,
  4087. time = this.options.toTime(x);
  4088. this.setCustomTime(time);
  4089. // fire a timechange event
  4090. this.emit('timechange', {
  4091. time: new Date(this.customTime.valueOf())
  4092. });
  4093. event.stopPropagation();
  4094. event.preventDefault();
  4095. };
  4096. /**
  4097. * Stop moving operating.
  4098. * @param {event} event
  4099. * @private
  4100. */
  4101. CustomTime.prototype._onDragEnd = function (event) {
  4102. if (!this.eventParams.dragging) return;
  4103. // fire a timechanged event
  4104. this.emit('timechanged', {
  4105. time: new Date(this.customTime.valueOf())
  4106. });
  4107. event.stopPropagation();
  4108. event.preventDefault();
  4109. };
  4110. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  4111. /**
  4112. * An ItemSet holds a set of items and ranges which can be displayed in a
  4113. * range. The width is determined by the parent of the ItemSet, and the height
  4114. * is determined by the size of the items.
  4115. * @param {Panel} backgroundPanel Panel which can be used to display the
  4116. * vertical lines of box items.
  4117. * @param {Panel} axisPanel Panel on the axis where the dots of box-items
  4118. * can be displayed.
  4119. * @param {Panel} sidePanel Left side panel holding labels
  4120. * @param {Object} [options] See ItemSet.setOptions for the available options.
  4121. * @constructor ItemSet
  4122. * @extends Panel
  4123. */
  4124. function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
  4125. this.id = util.randomUUID();
  4126. // one options object is shared by this itemset and all its items
  4127. this.options = options || {};
  4128. this.backgroundPanel = backgroundPanel;
  4129. this.axisPanel = axisPanel;
  4130. this.sidePanel = sidePanel;
  4131. this.itemOptions = Object.create(this.options);
  4132. this.dom = {};
  4133. this.hammer = null;
  4134. var me = this;
  4135. this.itemsData = null; // DataSet
  4136. this.groupsData = null; // DataSet
  4137. this.range = null; // Range or Object {start: number, end: number}
  4138. // listeners for the DataSet of the items
  4139. this.itemListeners = {
  4140. 'add': function (event, params, senderId) {
  4141. if (senderId != me.id) me._onAdd(params.items);
  4142. },
  4143. 'update': function (event, params, senderId) {
  4144. if (senderId != me.id) me._onUpdate(params.items);
  4145. },
  4146. 'remove': function (event, params, senderId) {
  4147. if (senderId != me.id) me._onRemove(params.items);
  4148. }
  4149. };
  4150. // listeners for the DataSet of the groups
  4151. this.groupListeners = {
  4152. 'add': function (event, params, senderId) {
  4153. if (senderId != me.id) me._onAddGroups(params.items);
  4154. },
  4155. 'update': function (event, params, senderId) {
  4156. if (senderId != me.id) me._onUpdateGroups(params.items);
  4157. },
  4158. 'remove': function (event, params, senderId) {
  4159. if (senderId != me.id) me._onRemoveGroups(params.items);
  4160. }
  4161. };
  4162. this.items = {}; // object with an Item for every data item
  4163. this.groups = {}; // Group object for every group
  4164. this.groupIds = [];
  4165. this.selection = []; // list with the ids of all selected nodes
  4166. this.stackDirty = true; // if true, all items will be restacked on next repaint
  4167. this.touchParams = {}; // stores properties while dragging
  4168. // create the HTML DOM
  4169. this._create();
  4170. }
  4171. ItemSet.prototype = new Panel();
  4172. // available item types will be registered here
  4173. ItemSet.types = {
  4174. box: ItemBox,
  4175. range: ItemRange,
  4176. rangeoverflow: ItemRangeOverflow,
  4177. point: ItemPoint
  4178. };
  4179. /**
  4180. * Create the HTML DOM for the ItemSet
  4181. */
  4182. ItemSet.prototype._create = function _create(){
  4183. var frame = document.createElement('div');
  4184. frame['timeline-itemset'] = this;
  4185. this.frame = frame;
  4186. // create background panel
  4187. var background = document.createElement('div');
  4188. background.className = 'background';
  4189. this.backgroundPanel.frame.appendChild(background);
  4190. this.dom.background = background;
  4191. // create foreground panel
  4192. var foreground = document.createElement('div');
  4193. foreground.className = 'foreground';
  4194. frame.appendChild(foreground);
  4195. this.dom.foreground = foreground;
  4196. // create axis panel
  4197. var axis = document.createElement('div');
  4198. axis.className = 'axis';
  4199. this.dom.axis = axis;
  4200. this.axisPanel.frame.appendChild(axis);
  4201. // create labelset
  4202. var labelSet = document.createElement('div');
  4203. labelSet.className = 'labelset';
  4204. this.dom.labelSet = labelSet;
  4205. this.sidePanel.frame.appendChild(labelSet);
  4206. // create ungrouped Group
  4207. this._updateUngrouped();
  4208. // attach event listeners
  4209. // TODO: use event listeners from the rootpanel to improve performance?
  4210. this.hammer = Hammer(frame, {
  4211. prevent_default: true
  4212. });
  4213. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4214. this.hammer.on('drag', this._onDrag.bind(this));
  4215. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4216. };
  4217. /**
  4218. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4219. * @param {Object} [options] The following options are available:
  4220. * {String | function} [className]
  4221. * class name for the itemset
  4222. * {String} [type]
  4223. * Default type for the items. Choose from 'box'
  4224. * (default), 'point', or 'range'. The default
  4225. * Style can be overwritten by individual items.
  4226. * {String} align
  4227. * Alignment for the items, only applicable for
  4228. * ItemBox. Choose 'center' (default), 'left', or
  4229. * 'right'.
  4230. * {String} orientation
  4231. * Orientation of the item set. Choose 'top' or
  4232. * 'bottom' (default).
  4233. * {Number} margin.axis
  4234. * Margin between the axis and the items in pixels.
  4235. * Default is 20.
  4236. * {Number} margin.item
  4237. * Margin between items in pixels. Default is 10.
  4238. * {Number} padding
  4239. * Padding of the contents of an item in pixels.
  4240. * Must correspond with the items css. Default is 5.
  4241. * {Function} snap
  4242. * Function to let items snap to nice dates when
  4243. * dragging items.
  4244. */
  4245. ItemSet.prototype.setOptions = function setOptions(options) {
  4246. Component.prototype.setOptions.call(this, options);
  4247. };
  4248. /**
  4249. * Mark the ItemSet dirty so it will refresh everything with next repaint
  4250. */
  4251. ItemSet.prototype.markDirty = function markDirty() {
  4252. this.groupIds = [];
  4253. this.stackDirty = true;
  4254. };
  4255. /**
  4256. * Hide the component from the DOM
  4257. */
  4258. ItemSet.prototype.hide = function hide() {
  4259. // remove the axis with dots
  4260. if (this.dom.axis.parentNode) {
  4261. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4262. }
  4263. // remove the background with vertical lines
  4264. if (this.dom.background.parentNode) {
  4265. this.dom.background.parentNode.removeChild(this.dom.background);
  4266. }
  4267. // remove the labelset containing all group labels
  4268. if (this.dom.labelSet.parentNode) {
  4269. this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
  4270. }
  4271. };
  4272. /**
  4273. * Show the component in the DOM (when not already visible).
  4274. * @return {Boolean} changed
  4275. */
  4276. ItemSet.prototype.show = function show() {
  4277. // show axis with dots
  4278. if (!this.dom.axis.parentNode) {
  4279. this.axisPanel.frame.appendChild(this.dom.axis);
  4280. }
  4281. // show background with vertical lines
  4282. if (!this.dom.background.parentNode) {
  4283. this.backgroundPanel.frame.appendChild(this.dom.background);
  4284. }
  4285. // show labelset containing labels
  4286. if (!this.dom.labelSet.parentNode) {
  4287. this.sidePanel.frame.appendChild(this.dom.labelSet);
  4288. }
  4289. };
  4290. /**
  4291. * Set range (start and end).
  4292. * @param {Range | Object} range A Range or an object containing start and end.
  4293. */
  4294. ItemSet.prototype.setRange = function setRange(range) {
  4295. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4296. throw new TypeError('Range must be an instance of Range, ' +
  4297. 'or an object containing start and end.');
  4298. }
  4299. this.range = range;
  4300. };
  4301. /**
  4302. * Set selected items by their id. Replaces the current selection
  4303. * Unknown id's are silently ignored.
  4304. * @param {Array} [ids] An array with zero or more id's of the items to be
  4305. * selected. If ids is an empty array, all items will be
  4306. * unselected.
  4307. */
  4308. ItemSet.prototype.setSelection = function setSelection(ids) {
  4309. var i, ii, id, item;
  4310. if (ids) {
  4311. if (!Array.isArray(ids)) {
  4312. throw new TypeError('Array expected');
  4313. }
  4314. // unselect currently selected items
  4315. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4316. id = this.selection[i];
  4317. item = this.items[id];
  4318. if (item) item.unselect();
  4319. }
  4320. // select items
  4321. this.selection = [];
  4322. for (i = 0, ii = ids.length; i < ii; i++) {
  4323. id = ids[i];
  4324. item = this.items[id];
  4325. if (item) {
  4326. this.selection.push(id);
  4327. item.select();
  4328. }
  4329. }
  4330. }
  4331. };
  4332. /**
  4333. * Get the selected items by their id
  4334. * @return {Array} ids The ids of the selected items
  4335. */
  4336. ItemSet.prototype.getSelection = function getSelection() {
  4337. return this.selection.concat([]);
  4338. };
  4339. /**
  4340. * Deselect a selected item
  4341. * @param {String | Number} id
  4342. * @private
  4343. */
  4344. ItemSet.prototype._deselect = function _deselect(id) {
  4345. var selection = this.selection;
  4346. for (var i = 0, ii = selection.length; i < ii; i++) {
  4347. if (selection[i] == id) { // non-strict comparison!
  4348. selection.splice(i, 1);
  4349. break;
  4350. }
  4351. }
  4352. };
  4353. /**
  4354. * Return the item sets frame
  4355. * @returns {HTMLElement} frame
  4356. */
  4357. ItemSet.prototype.getFrame = function getFrame() {
  4358. return this.frame;
  4359. };
  4360. /**
  4361. * Repaint the component
  4362. * @return {boolean} Returns true if the component is resized
  4363. */
  4364. ItemSet.prototype.repaint = function repaint() {
  4365. var margin = this.options.margin,
  4366. range = this.range,
  4367. asSize = util.option.asSize,
  4368. asString = util.option.asString,
  4369. options = this.options,
  4370. orientation = this.getOption('orientation'),
  4371. resized = false,
  4372. frame = this.frame;
  4373. // TODO: document this feature to specify one margin for both item and axis distance
  4374. if (typeof margin === 'number') {
  4375. margin = {
  4376. item: margin,
  4377. axis: margin
  4378. };
  4379. }
  4380. // update className
  4381. frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
  4382. // reorder the groups (if needed)
  4383. resized = this._orderGroups() || resized;
  4384. // check whether zoomed (in that case we need to re-stack everything)
  4385. // TODO: would be nicer to get this as a trigger from Range
  4386. var visibleInterval = this.range.end - this.range.start;
  4387. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  4388. if (zoomed) this.stackDirty = true;
  4389. this.lastVisibleInterval = visibleInterval;
  4390. this.lastWidth = this.width;
  4391. // repaint all groups
  4392. var restack = this.stackDirty,
  4393. firstGroup = this._firstGroup(),
  4394. firstMargin = {
  4395. item: margin.item,
  4396. axis: margin.axis
  4397. },
  4398. nonFirstMargin = {
  4399. item: margin.item,
  4400. axis: margin.item / 2
  4401. },
  4402. height = 0,
  4403. minHeight = margin.axis + margin.item;
  4404. util.forEach(this.groups, function (group) {
  4405. var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
  4406. resized = group.repaint(range, groupMargin, restack) || resized;
  4407. height += group.height;
  4408. });
  4409. height = Math.max(height, minHeight);
  4410. this.stackDirty = false;
  4411. // reposition frame
  4412. frame.style.left = asSize(options.left, '');
  4413. frame.style.right = asSize(options.right, '');
  4414. frame.style.top = asSize((orientation == 'top') ? '0' : '');
  4415. frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4416. frame.style.width = asSize(options.width, '100%');
  4417. frame.style.height = asSize(height);
  4418. //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
  4419. // calculate actual size and position
  4420. this.top = frame.offsetTop;
  4421. this.left = frame.offsetLeft;
  4422. this.width = frame.offsetWidth;
  4423. this.height = height;
  4424. // reposition axis
  4425. this.dom.axis.style.left = asSize(options.left, '0');
  4426. this.dom.axis.style.right = asSize(options.right, '');
  4427. this.dom.axis.style.width = asSize(options.width, '100%');
  4428. this.dom.axis.style.height = asSize(0);
  4429. this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
  4430. this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4431. // check if this component is resized
  4432. resized = this._isResized() || resized;
  4433. return resized;
  4434. };
  4435. /**
  4436. * Get the first group, aligned with the axis
  4437. * @return {Group | null} firstGroup
  4438. * @private
  4439. */
  4440. ItemSet.prototype._firstGroup = function _firstGroup() {
  4441. var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
  4442. var firstGroupId = this.groupIds[firstGroupIndex];
  4443. var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
  4444. return firstGroup || null;
  4445. };
  4446. /**
  4447. * Create or delete the group holding all ungrouped items. This group is used when
  4448. * there are no groups specified.
  4449. * @protected
  4450. */
  4451. ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
  4452. var ungrouped = this.groups[UNGROUPED];
  4453. if (this.groupsData) {
  4454. // remove the group holding all ungrouped items
  4455. if (ungrouped) {
  4456. ungrouped.hide();
  4457. delete this.groups[UNGROUPED];
  4458. }
  4459. }
  4460. else {
  4461. // create a group holding all (unfiltered) items
  4462. if (!ungrouped) {
  4463. var id = null;
  4464. var data = null;
  4465. ungrouped = new Group(id, data, this);
  4466. this.groups[UNGROUPED] = ungrouped;
  4467. for (var itemId in this.items) {
  4468. if (this.items.hasOwnProperty(itemId)) {
  4469. ungrouped.add(this.items[itemId]);
  4470. }
  4471. }
  4472. ungrouped.show();
  4473. }
  4474. }
  4475. };
  4476. /**
  4477. * Get the foreground container element
  4478. * @return {HTMLElement} foreground
  4479. */
  4480. ItemSet.prototype.getForeground = function getForeground() {
  4481. return this.dom.foreground;
  4482. };
  4483. /**
  4484. * Get the background container element
  4485. * @return {HTMLElement} background
  4486. */
  4487. ItemSet.prototype.getBackground = function getBackground() {
  4488. return this.dom.background;
  4489. };
  4490. /**
  4491. * Get the axis container element
  4492. * @return {HTMLElement} axis
  4493. */
  4494. ItemSet.prototype.getAxis = function getAxis() {
  4495. return this.dom.axis;
  4496. };
  4497. /**
  4498. * Get the element for the labelset
  4499. * @return {HTMLElement} labelSet
  4500. */
  4501. ItemSet.prototype.getLabelSet = function getLabelSet() {
  4502. return this.dom.labelSet;
  4503. };
  4504. /**
  4505. * Set items
  4506. * @param {vis.DataSet | null} items
  4507. */
  4508. ItemSet.prototype.setItems = function setItems(items) {
  4509. var me = this,
  4510. ids,
  4511. oldItemsData = this.itemsData;
  4512. // replace the dataset
  4513. if (!items) {
  4514. this.itemsData = null;
  4515. }
  4516. else if (items instanceof DataSet || items instanceof DataView) {
  4517. this.itemsData = items;
  4518. }
  4519. else {
  4520. throw new TypeError('Data must be an instance of DataSet or DataView');
  4521. }
  4522. if (oldItemsData) {
  4523. // unsubscribe from old dataset
  4524. util.forEach(this.itemListeners, function (callback, event) {
  4525. oldItemsData.unsubscribe(event, callback);
  4526. });
  4527. // remove all drawn items
  4528. ids = oldItemsData.getIds();
  4529. this._onRemove(ids);
  4530. }
  4531. if (this.itemsData) {
  4532. // subscribe to new dataset
  4533. var id = this.id;
  4534. util.forEach(this.itemListeners, function (callback, event) {
  4535. me.itemsData.on(event, callback, id);
  4536. });
  4537. // add all new items
  4538. ids = this.itemsData.getIds();
  4539. this._onAdd(ids);
  4540. // update the group holding all ungrouped items
  4541. this._updateUngrouped();
  4542. }
  4543. };
  4544. /**
  4545. * Get the current items
  4546. * @returns {vis.DataSet | null}
  4547. */
  4548. ItemSet.prototype.getItems = function getItems() {
  4549. return this.itemsData;
  4550. };
  4551. /**
  4552. * Set groups
  4553. * @param {vis.DataSet} groups
  4554. */
  4555. ItemSet.prototype.setGroups = function setGroups(groups) {
  4556. var me = this,
  4557. ids;
  4558. // unsubscribe from current dataset
  4559. if (this.groupsData) {
  4560. util.forEach(this.groupListeners, function (callback, event) {
  4561. me.groupsData.unsubscribe(event, callback);
  4562. });
  4563. // remove all drawn groups
  4564. ids = this.groupsData.getIds();
  4565. this._onRemoveGroups(ids);
  4566. }
  4567. // replace the dataset
  4568. if (!groups) {
  4569. this.groupsData = null;
  4570. }
  4571. else if (groups instanceof DataSet || groups instanceof DataView) {
  4572. this.groupsData = groups;
  4573. }
  4574. else {
  4575. throw new TypeError('Data must be an instance of DataSet or DataView');
  4576. }
  4577. if (this.groupsData) {
  4578. // subscribe to new dataset
  4579. var id = this.id;
  4580. util.forEach(this.groupListeners, function (callback, event) {
  4581. me.groupsData.on(event, callback, id);
  4582. });
  4583. // draw all ms
  4584. ids = this.groupsData.getIds();
  4585. this._onAddGroups(ids);
  4586. }
  4587. // update the group holding all ungrouped items
  4588. this._updateUngrouped();
  4589. // update the order of all items in each group
  4590. this._order();
  4591. this.emit('change');
  4592. };
  4593. /**
  4594. * Get the current groups
  4595. * @returns {vis.DataSet | null} groups
  4596. */
  4597. ItemSet.prototype.getGroups = function getGroups() {
  4598. return this.groupsData;
  4599. };
  4600. /**
  4601. * Remove an item by its id
  4602. * @param {String | Number} id
  4603. */
  4604. ItemSet.prototype.removeItem = function removeItem (id) {
  4605. var item = this.itemsData.get(id),
  4606. dataset = this._myDataSet();
  4607. if (item) {
  4608. // confirm deletion
  4609. this.options.onRemove(item, function (item) {
  4610. if (item) {
  4611. // remove by id here, it is possible that an item has no id defined
  4612. // itself, so better not delete by the item itself
  4613. dataset.remove(id);
  4614. }
  4615. });
  4616. }
  4617. };
  4618. /**
  4619. * Handle updated items
  4620. * @param {Number[]} ids
  4621. * @protected
  4622. */
  4623. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4624. var me = this,
  4625. items = this.items,
  4626. itemOptions = this.itemOptions;
  4627. ids.forEach(function (id) {
  4628. var itemData = me.itemsData.get(id),
  4629. item = items[id],
  4630. type = itemData.type ||
  4631. (itemData.start && itemData.end && 'range') ||
  4632. me.options.type ||
  4633. 'box';
  4634. var constructor = ItemSet.types[type];
  4635. if (item) {
  4636. // update item
  4637. if (!constructor || !(item instanceof constructor)) {
  4638. // item type has changed, delete the item and recreate it
  4639. me._removeItem(item);
  4640. item = null;
  4641. }
  4642. else {
  4643. me._updateItem(item, itemData);
  4644. }
  4645. }
  4646. if (!item) {
  4647. // create item
  4648. if (constructor) {
  4649. item = new constructor(itemData, me.options, itemOptions);
  4650. item.id = id; // TODO: not so nice setting id afterwards
  4651. me._addItem(item);
  4652. }
  4653. else {
  4654. throw new TypeError('Unknown item type "' + type + '"');
  4655. }
  4656. }
  4657. });
  4658. this._order();
  4659. this.stackDirty = true; // force re-stacking of all items next repaint
  4660. this.emit('change');
  4661. };
  4662. /**
  4663. * Handle added items
  4664. * @param {Number[]} ids
  4665. * @protected
  4666. */
  4667. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  4668. /**
  4669. * Handle removed items
  4670. * @param {Number[]} ids
  4671. * @protected
  4672. */
  4673. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4674. var count = 0;
  4675. var me = this;
  4676. ids.forEach(function (id) {
  4677. var item = me.items[id];
  4678. if (item) {
  4679. count++;
  4680. me._removeItem(item);
  4681. }
  4682. });
  4683. if (count) {
  4684. // update order
  4685. this._order();
  4686. this.stackDirty = true; // force re-stacking of all items next repaint
  4687. this.emit('change');
  4688. }
  4689. };
  4690. /**
  4691. * Update the order of item in all groups
  4692. * @private
  4693. */
  4694. ItemSet.prototype._order = function _order() {
  4695. // reorder the items in all groups
  4696. // TODO: optimization: only reorder groups affected by the changed items
  4697. util.forEach(this.groups, function (group) {
  4698. group.order();
  4699. });
  4700. };
  4701. /**
  4702. * Handle updated groups
  4703. * @param {Number[]} ids
  4704. * @private
  4705. */
  4706. ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
  4707. this._onAddGroups(ids);
  4708. };
  4709. /**
  4710. * Handle changed groups
  4711. * @param {Number[]} ids
  4712. * @private
  4713. */
  4714. ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
  4715. var me = this;
  4716. ids.forEach(function (id) {
  4717. var groupData = me.groupsData.get(id);
  4718. var group = me.groups[id];
  4719. if (!group) {
  4720. // check for reserved ids
  4721. if (id == UNGROUPED) {
  4722. throw new Error('Illegal group id. ' + id + ' is a reserved id.');
  4723. }
  4724. var groupOptions = Object.create(me.options);
  4725. util.extend(groupOptions, {
  4726. height: null
  4727. });
  4728. group = new Group(id, groupData, me);
  4729. me.groups[id] = group;
  4730. // add items with this groupId to the new group
  4731. for (var itemId in me.items) {
  4732. if (me.items.hasOwnProperty(itemId)) {
  4733. var item = me.items[itemId];
  4734. if (item.data.group == id) {
  4735. group.add(item);
  4736. }
  4737. }
  4738. }
  4739. group.order();
  4740. group.show();
  4741. }
  4742. else {
  4743. // update group
  4744. group.setData(groupData);
  4745. }
  4746. });
  4747. this.emit('change');
  4748. };
  4749. /**
  4750. * Handle removed groups
  4751. * @param {Number[]} ids
  4752. * @private
  4753. */
  4754. ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
  4755. var groups = this.groups;
  4756. ids.forEach(function (id) {
  4757. var group = groups[id];
  4758. if (group) {
  4759. group.hide();
  4760. delete groups[id];
  4761. }
  4762. });
  4763. this.markDirty();
  4764. this.emit('change');
  4765. };
  4766. /**
  4767. * Reorder the groups if needed
  4768. * @return {boolean} changed
  4769. * @private
  4770. */
  4771. ItemSet.prototype._orderGroups = function () {
  4772. if (this.groupsData) {
  4773. // reorder the groups
  4774. var groupIds = this.groupsData.getIds({
  4775. order: this.options.groupOrder
  4776. });
  4777. var changed = !util.equalArray(groupIds, this.groupIds);
  4778. if (changed) {
  4779. // hide all groups, removes them from the DOM
  4780. var groups = this.groups;
  4781. groupIds.forEach(function (groupId) {
  4782. var group = groups[groupId];
  4783. group.hide();
  4784. });
  4785. // show the groups again, attach them to the DOM in correct order
  4786. groupIds.forEach(function (groupId) {
  4787. groups[groupId].show();
  4788. });
  4789. this.groupIds = groupIds;
  4790. }
  4791. return changed;
  4792. }
  4793. else {
  4794. return false;
  4795. }
  4796. };
  4797. /**
  4798. * Add a new item
  4799. * @param {Item} item
  4800. * @private
  4801. */
  4802. ItemSet.prototype._addItem = function _addItem(item) {
  4803. this.items[item.id] = item;
  4804. // add to group
  4805. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  4806. var group = this.groups[groupId];
  4807. if (group) group.add(item);
  4808. };
  4809. /**
  4810. * Update an existing item
  4811. * @param {Item} item
  4812. * @param {Object} itemData
  4813. * @private
  4814. */
  4815. ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
  4816. var oldGroupId = item.data.group;
  4817. item.data = itemData;
  4818. if (item.displayed) {
  4819. item.repaint();
  4820. }
  4821. // update group
  4822. if (oldGroupId != item.data.group) {
  4823. var oldGroup = this.groups[oldGroupId];
  4824. if (oldGroup) oldGroup.remove(item);
  4825. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  4826. var group = this.groups[groupId];
  4827. if (group) group.add(item);
  4828. }
  4829. };
  4830. /**
  4831. * Delete an item from the ItemSet: remove it from the DOM, from the map
  4832. * with items, and from the map with visible items, and from the selection
  4833. * @param {Item} item
  4834. * @private
  4835. */
  4836. ItemSet.prototype._removeItem = function _removeItem(item) {
  4837. // remove from DOM
  4838. item.hide();
  4839. // remove from items
  4840. delete this.items[item.id];
  4841. // remove from selection
  4842. var index = this.selection.indexOf(item.id);
  4843. if (index != -1) this.selection.splice(index, 1);
  4844. // remove from group
  4845. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  4846. var group = this.groups[groupId];
  4847. if (group) group.remove(item);
  4848. };
  4849. /**
  4850. * Create an array containing all items being a range (having an end date)
  4851. * @param array
  4852. * @returns {Array}
  4853. * @private
  4854. */
  4855. ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
  4856. var endArray = [];
  4857. for (var i = 0; i < array.length; i++) {
  4858. if (array[i] instanceof ItemRange) {
  4859. endArray.push(array[i]);
  4860. }
  4861. }
  4862. return endArray;
  4863. };
  4864. /**
  4865. * Get the width of the group labels
  4866. * @return {Number} width
  4867. */
  4868. ItemSet.prototype.getLabelsWidth = function getLabelsWidth() {
  4869. var width = 0;
  4870. util.forEach(this.groups, function (group) {
  4871. width = Math.max(width, group.getLabelWidth());
  4872. });
  4873. return width;
  4874. };
  4875. /**
  4876. * Get the height of the itemsets background
  4877. * @return {Number} height
  4878. */
  4879. ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
  4880. return this.height;
  4881. };
  4882. /**
  4883. * Start dragging the selected events
  4884. * @param {Event} event
  4885. * @private
  4886. */
  4887. ItemSet.prototype._onDragStart = function (event) {
  4888. if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
  4889. return;
  4890. }
  4891. var item = ItemSet.itemFromTarget(event),
  4892. me = this,
  4893. props;
  4894. if (item && item.selected) {
  4895. var dragLeftItem = event.target.dragLeftItem;
  4896. var dragRightItem = event.target.dragRightItem;
  4897. if (dragLeftItem) {
  4898. props = {
  4899. item: dragLeftItem
  4900. };
  4901. if (me.options.editable.updateTime) {
  4902. props.start = item.data.start.valueOf();
  4903. }
  4904. if (me.options.editable.updateGroup) {
  4905. if ('group' in item.data) props.group = item.data.group;
  4906. }
  4907. this.touchParams.itemProps = [props];
  4908. }
  4909. else if (dragRightItem) {
  4910. props = {
  4911. item: dragRightItem
  4912. };
  4913. if (me.options.editable.updateTime) {
  4914. props.end = item.data.end.valueOf();
  4915. }
  4916. if (me.options.editable.updateGroup) {
  4917. if ('group' in item.data) props.group = item.data.group;
  4918. }
  4919. this.touchParams.itemProps = [props];
  4920. }
  4921. else {
  4922. this.touchParams.itemProps = this.getSelection().map(function (id) {
  4923. var item = me.items[id];
  4924. var props = {
  4925. item: item
  4926. };
  4927. if (me.options.editable.updateTime) {
  4928. if ('start' in item.data) props.start = item.data.start.valueOf();
  4929. if ('end' in item.data) props.end = item.data.end.valueOf();
  4930. }
  4931. if (me.options.editable.updateGroup) {
  4932. if ('group' in item.data) props.group = item.data.group;
  4933. }
  4934. return props;
  4935. });
  4936. }
  4937. event.stopPropagation();
  4938. }
  4939. };
  4940. /**
  4941. * Drag selected items
  4942. * @param {Event} event
  4943. * @private
  4944. */
  4945. ItemSet.prototype._onDrag = function (event) {
  4946. if (this.touchParams.itemProps) {
  4947. var snap = this.options.snap || null,
  4948. deltaX = event.gesture.deltaX,
  4949. scale = (this.width / (this.range.end - this.range.start)),
  4950. offset = deltaX / scale;
  4951. // move
  4952. this.touchParams.itemProps.forEach(function (props) {
  4953. if ('start' in props) {
  4954. var start = new Date(props.start + offset);
  4955. props.item.data.start = snap ? snap(start) : start;
  4956. }
  4957. if ('end' in props) {
  4958. var end = new Date(props.end + offset);
  4959. props.item.data.end = snap ? snap(end) : end;
  4960. }
  4961. if ('group' in props) {
  4962. // drag from one group to another
  4963. var group = ItemSet.groupFromTarget(event);
  4964. if (group && group.groupId != props.item.data.group) {
  4965. var oldGroup = props.item.parent;
  4966. oldGroup.remove(props.item);
  4967. oldGroup.order();
  4968. group.add(props.item);
  4969. group.order();
  4970. props.item.data.group = group.groupId;
  4971. }
  4972. }
  4973. });
  4974. // TODO: implement onMoving handler
  4975. this.stackDirty = true; // force re-stacking of all items next repaint
  4976. this.emit('change');
  4977. event.stopPropagation();
  4978. }
  4979. };
  4980. /**
  4981. * End of dragging selected items
  4982. * @param {Event} event
  4983. * @private
  4984. */
  4985. ItemSet.prototype._onDragEnd = function (event) {
  4986. if (this.touchParams.itemProps) {
  4987. // prepare a change set for the changed items
  4988. var changes = [],
  4989. me = this,
  4990. dataset = this._myDataSet();
  4991. this.touchParams.itemProps.forEach(function (props) {
  4992. var id = props.item.id,
  4993. itemData = me.itemsData.get(id);
  4994. var changed = false;
  4995. if ('start' in props.item.data) {
  4996. changed = (props.start != props.item.data.start.valueOf());
  4997. itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
  4998. }
  4999. if ('end' in props.item.data) {
  5000. changed = changed || (props.end != props.item.data.end.valueOf());
  5001. itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
  5002. }
  5003. if ('group' in props.item.data) {
  5004. changed = changed || (props.group != props.item.data.group);
  5005. itemData.group = props.item.data.group;
  5006. }
  5007. // only apply changes when start or end is actually changed
  5008. if (changed) {
  5009. me.options.onMove(itemData, function (itemData) {
  5010. if (itemData) {
  5011. // apply changes
  5012. itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
  5013. changes.push(itemData);
  5014. }
  5015. else {
  5016. // restore original values
  5017. if ('start' in props) props.item.data.start = props.start;
  5018. if ('end' in props) props.item.data.end = props.end;
  5019. me.stackDirty = true; // force re-stacking of all items next repaint
  5020. me.emit('change');
  5021. }
  5022. });
  5023. }
  5024. });
  5025. this.touchParams.itemProps = null;
  5026. // apply the changes to the data (if there are changes)
  5027. if (changes.length) {
  5028. dataset.update(changes);
  5029. }
  5030. event.stopPropagation();
  5031. }
  5032. };
  5033. /**
  5034. * Find an item from an event target:
  5035. * searches for the attribute 'timeline-item' in the event target's element tree
  5036. * @param {Event} event
  5037. * @return {Item | null} item
  5038. */
  5039. ItemSet.itemFromTarget = function itemFromTarget (event) {
  5040. var target = event.target;
  5041. while (target) {
  5042. if (target.hasOwnProperty('timeline-item')) {
  5043. return target['timeline-item'];
  5044. }
  5045. target = target.parentNode;
  5046. }
  5047. return null;
  5048. };
  5049. /**
  5050. * Find the Group from an event target:
  5051. * searches for the attribute 'timeline-group' in the event target's element tree
  5052. * @param {Event} event
  5053. * @return {Group | null} group
  5054. */
  5055. ItemSet.groupFromTarget = function groupFromTarget (event) {
  5056. var target = event.target;
  5057. while (target) {
  5058. if (target.hasOwnProperty('timeline-group')) {
  5059. return target['timeline-group'];
  5060. }
  5061. target = target.parentNode;
  5062. }
  5063. return null;
  5064. };
  5065. /**
  5066. * Find the ItemSet from an event target:
  5067. * searches for the attribute 'timeline-itemset' in the event target's element tree
  5068. * @param {Event} event
  5069. * @return {ItemSet | null} item
  5070. */
  5071. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  5072. var target = event.target;
  5073. while (target) {
  5074. if (target.hasOwnProperty('timeline-itemset')) {
  5075. return target['timeline-itemset'];
  5076. }
  5077. target = target.parentNode;
  5078. }
  5079. return null;
  5080. };
  5081. /**
  5082. * Find the DataSet to which this ItemSet is connected
  5083. * @returns {null | DataSet} dataset
  5084. * @private
  5085. */
  5086. ItemSet.prototype._myDataSet = function _myDataSet() {
  5087. // find the root DataSet
  5088. var dataset = this.itemsData;
  5089. while (dataset instanceof DataView) {
  5090. dataset = dataset.data;
  5091. }
  5092. return dataset;
  5093. };
  5094. /**
  5095. * @constructor Item
  5096. * @param {Object} data Object containing (optional) parameters type,
  5097. * start, end, content, group, className.
  5098. * @param {Object} [options] Options to set initial property values
  5099. * @param {Object} [defaultOptions] default options
  5100. * // TODO: describe available options
  5101. */
  5102. function Item (data, options, defaultOptions) {
  5103. this.id = null;
  5104. this.parent = null;
  5105. this.data = data;
  5106. this.dom = null;
  5107. this.options = options || {};
  5108. this.defaultOptions = defaultOptions || {};
  5109. this.selected = false;
  5110. this.displayed = false;
  5111. this.dirty = true;
  5112. this.top = null;
  5113. this.left = null;
  5114. this.width = null;
  5115. this.height = null;
  5116. }
  5117. /**
  5118. * Select current item
  5119. */
  5120. Item.prototype.select = function select() {
  5121. this.selected = true;
  5122. if (this.displayed) this.repaint();
  5123. };
  5124. /**
  5125. * Unselect current item
  5126. */
  5127. Item.prototype.unselect = function unselect() {
  5128. this.selected = false;
  5129. if (this.displayed) this.repaint();
  5130. };
  5131. /**
  5132. * Set a parent for the item
  5133. * @param {ItemSet | Group} parent
  5134. */
  5135. Item.prototype.setParent = function setParent(parent) {
  5136. if (this.displayed) {
  5137. this.hide();
  5138. this.parent = parent;
  5139. if (this.parent) {
  5140. this.show();
  5141. }
  5142. }
  5143. else {
  5144. this.parent = parent;
  5145. }
  5146. };
  5147. /**
  5148. * Check whether this item is visible inside given range
  5149. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5150. * @returns {boolean} True if visible
  5151. */
  5152. Item.prototype.isVisible = function isVisible (range) {
  5153. // Should be implemented by Item implementations
  5154. return false;
  5155. };
  5156. /**
  5157. * Show the Item in the DOM (when not already visible)
  5158. * @return {Boolean} changed
  5159. */
  5160. Item.prototype.show = function show() {
  5161. return false;
  5162. };
  5163. /**
  5164. * Hide the Item from the DOM (when visible)
  5165. * @return {Boolean} changed
  5166. */
  5167. Item.prototype.hide = function hide() {
  5168. return false;
  5169. };
  5170. /**
  5171. * Repaint the item
  5172. */
  5173. Item.prototype.repaint = function repaint() {
  5174. // should be implemented by the item
  5175. };
  5176. /**
  5177. * Reposition the Item horizontally
  5178. */
  5179. Item.prototype.repositionX = function repositionX() {
  5180. // should be implemented by the item
  5181. };
  5182. /**
  5183. * Reposition the Item vertically
  5184. */
  5185. Item.prototype.repositionY = function repositionY() {
  5186. // should be implemented by the item
  5187. };
  5188. /**
  5189. * Repaint a delete button on the top right of the item when the item is selected
  5190. * @param {HTMLElement} anchor
  5191. * @protected
  5192. */
  5193. Item.prototype._repaintDeleteButton = function (anchor) {
  5194. if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
  5195. // create and show button
  5196. var me = this;
  5197. var deleteButton = document.createElement('div');
  5198. deleteButton.className = 'delete';
  5199. deleteButton.title = 'Delete this item';
  5200. Hammer(deleteButton, {
  5201. preventDefault: true
  5202. }).on('tap', function (event) {
  5203. me.parent.removeFromDataSet(me);
  5204. event.stopPropagation();
  5205. });
  5206. anchor.appendChild(deleteButton);
  5207. this.dom.deleteButton = deleteButton;
  5208. }
  5209. else if (!this.selected && this.dom.deleteButton) {
  5210. // remove button
  5211. if (this.dom.deleteButton.parentNode) {
  5212. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  5213. }
  5214. this.dom.deleteButton = null;
  5215. }
  5216. };
  5217. /**
  5218. * @constructor ItemBox
  5219. * @extends Item
  5220. * @param {Object} data Object containing parameters start
  5221. * content, className.
  5222. * @param {Object} [options] Options to set initial property values
  5223. * @param {Object} [defaultOptions] default options
  5224. * // TODO: describe available options
  5225. */
  5226. function ItemBox (data, options, defaultOptions) {
  5227. this.props = {
  5228. dot: {
  5229. width: 0,
  5230. height: 0
  5231. },
  5232. line: {
  5233. width: 0,
  5234. height: 0
  5235. }
  5236. };
  5237. // validate data
  5238. if (data) {
  5239. if (data.start == undefined) {
  5240. throw new Error('Property "start" missing in item ' + data);
  5241. }
  5242. }
  5243. Item.call(this, data, options, defaultOptions);
  5244. }
  5245. ItemBox.prototype = new Item (null);
  5246. /**
  5247. * Check whether this item is visible inside given range
  5248. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5249. * @returns {boolean} True if visible
  5250. */
  5251. ItemBox.prototype.isVisible = function isVisible (range) {
  5252. // determine visibility
  5253. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  5254. var interval = (range.end - range.start) / 4;
  5255. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  5256. };
  5257. /**
  5258. * Repaint the item
  5259. */
  5260. ItemBox.prototype.repaint = function repaint() {
  5261. var dom = this.dom;
  5262. if (!dom) {
  5263. // create DOM
  5264. this.dom = {};
  5265. dom = this.dom;
  5266. // create main box
  5267. dom.box = document.createElement('DIV');
  5268. // contents box (inside the background box). used for making margins
  5269. dom.content = document.createElement('DIV');
  5270. dom.content.className = 'content';
  5271. dom.box.appendChild(dom.content);
  5272. // line to axis
  5273. dom.line = document.createElement('DIV');
  5274. dom.line.className = 'line';
  5275. // dot on axis
  5276. dom.dot = document.createElement('DIV');
  5277. dom.dot.className = 'dot';
  5278. // attach this item as attribute
  5279. dom.box['timeline-item'] = this;
  5280. }
  5281. // append DOM to parent DOM
  5282. if (!this.parent) {
  5283. throw new Error('Cannot repaint item: no parent attached');
  5284. }
  5285. if (!dom.box.parentNode) {
  5286. var foreground = this.parent.getForeground();
  5287. if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5288. foreground.appendChild(dom.box);
  5289. }
  5290. if (!dom.line.parentNode) {
  5291. var background = this.parent.getBackground();
  5292. if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
  5293. background.appendChild(dom.line);
  5294. }
  5295. if (!dom.dot.parentNode) {
  5296. var axis = this.parent.getAxis();
  5297. if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
  5298. axis.appendChild(dom.dot);
  5299. }
  5300. this.displayed = true;
  5301. // update contents
  5302. if (this.data.content != this.content) {
  5303. this.content = this.data.content;
  5304. if (this.content instanceof Element) {
  5305. dom.content.innerHTML = '';
  5306. dom.content.appendChild(this.content);
  5307. }
  5308. else if (this.data.content != undefined) {
  5309. dom.content.innerHTML = this.content;
  5310. }
  5311. else {
  5312. throw new Error('Property "content" missing in item ' + this.data.id);
  5313. }
  5314. this.dirty = true;
  5315. }
  5316. // update class
  5317. var className = (this.data.className? ' ' + this.data.className : '') +
  5318. (this.selected ? ' selected' : '');
  5319. if (this.className != className) {
  5320. this.className = className;
  5321. dom.box.className = 'item box' + className;
  5322. dom.line.className = 'item line' + className;
  5323. dom.dot.className = 'item dot' + className;
  5324. this.dirty = true;
  5325. }
  5326. // recalculate size
  5327. if (this.dirty) {
  5328. this.props.dot.height = dom.dot.offsetHeight;
  5329. this.props.dot.width = dom.dot.offsetWidth;
  5330. this.props.line.width = dom.line.offsetWidth;
  5331. this.width = dom.box.offsetWidth;
  5332. this.height = dom.box.offsetHeight;
  5333. this.dirty = false;
  5334. }
  5335. this._repaintDeleteButton(dom.box);
  5336. };
  5337. /**
  5338. * Show the item in the DOM (when not already displayed). The items DOM will
  5339. * be created when needed.
  5340. */
  5341. ItemBox.prototype.show = function show() {
  5342. if (!this.displayed) {
  5343. this.repaint();
  5344. }
  5345. };
  5346. /**
  5347. * Hide the item from the DOM (when visible)
  5348. */
  5349. ItemBox.prototype.hide = function hide() {
  5350. if (this.displayed) {
  5351. var dom = this.dom;
  5352. if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
  5353. if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
  5354. if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
  5355. this.top = null;
  5356. this.left = null;
  5357. this.displayed = false;
  5358. }
  5359. };
  5360. /**
  5361. * Reposition the item horizontally
  5362. * @Override
  5363. */
  5364. ItemBox.prototype.repositionX = function repositionX() {
  5365. var start = this.defaultOptions.toScreen(this.data.start),
  5366. align = this.options.align || this.defaultOptions.align,
  5367. left,
  5368. box = this.dom.box,
  5369. line = this.dom.line,
  5370. dot = this.dom.dot;
  5371. // calculate left position of the box
  5372. if (align == 'right') {
  5373. this.left = start - this.width;
  5374. }
  5375. else if (align == 'left') {
  5376. this.left = start;
  5377. }
  5378. else {
  5379. // default or 'center'
  5380. this.left = start - this.width / 2;
  5381. }
  5382. // reposition box
  5383. box.style.left = this.left + 'px';
  5384. // reposition line
  5385. line.style.left = (start - this.props.line.width / 2) + 'px';
  5386. // reposition dot
  5387. dot.style.left = (start - this.props.dot.width / 2) + 'px';
  5388. };
  5389. /**
  5390. * Reposition the item vertically
  5391. * @Override
  5392. */
  5393. ItemBox.prototype.repositionY = function repositionY () {
  5394. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5395. box = this.dom.box,
  5396. line = this.dom.line,
  5397. dot = this.dom.dot;
  5398. if (orientation == 'top') {
  5399. box.style.top = (this.top || 0) + 'px';
  5400. box.style.bottom = '';
  5401. line.style.top = '0';
  5402. line.style.bottom = '';
  5403. line.style.height = (this.parent.top + this.top + 1) + 'px';
  5404. }
  5405. else { // orientation 'bottom'
  5406. box.style.top = '';
  5407. box.style.bottom = (this.top || 0) + 'px';
  5408. line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
  5409. line.style.bottom = '0';
  5410. line.style.height = '';
  5411. }
  5412. dot.style.top = (-this.props.dot.height / 2) + 'px';
  5413. };
  5414. /**
  5415. * @constructor ItemPoint
  5416. * @extends Item
  5417. * @param {Object} data Object containing parameters start
  5418. * content, className.
  5419. * @param {Object} [options] Options to set initial property values
  5420. * @param {Object} [defaultOptions] default options
  5421. * // TODO: describe available options
  5422. */
  5423. function ItemPoint (data, options, defaultOptions) {
  5424. this.props = {
  5425. dot: {
  5426. top: 0,
  5427. width: 0,
  5428. height: 0
  5429. },
  5430. content: {
  5431. height: 0,
  5432. marginLeft: 0
  5433. }
  5434. };
  5435. // validate data
  5436. if (data) {
  5437. if (data.start == undefined) {
  5438. throw new Error('Property "start" missing in item ' + data);
  5439. }
  5440. }
  5441. Item.call(this, data, options, defaultOptions);
  5442. }
  5443. ItemPoint.prototype = new Item (null);
  5444. /**
  5445. * Check whether this item is visible inside given range
  5446. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5447. * @returns {boolean} True if visible
  5448. */
  5449. ItemPoint.prototype.isVisible = function isVisible (range) {
  5450. // determine visibility
  5451. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  5452. var interval = (range.end - range.start) / 4;
  5453. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  5454. };
  5455. /**
  5456. * Repaint the item
  5457. */
  5458. ItemPoint.prototype.repaint = function repaint() {
  5459. var dom = this.dom;
  5460. if (!dom) {
  5461. // create DOM
  5462. this.dom = {};
  5463. dom = this.dom;
  5464. // background box
  5465. dom.point = document.createElement('div');
  5466. // className is updated in repaint()
  5467. // contents box, right from the dot
  5468. dom.content = document.createElement('div');
  5469. dom.content.className = 'content';
  5470. dom.point.appendChild(dom.content);
  5471. // dot at start
  5472. dom.dot = document.createElement('div');
  5473. dom.point.appendChild(dom.dot);
  5474. // attach this item as attribute
  5475. dom.point['timeline-item'] = this;
  5476. }
  5477. // append DOM to parent DOM
  5478. if (!this.parent) {
  5479. throw new Error('Cannot repaint item: no parent attached');
  5480. }
  5481. if (!dom.point.parentNode) {
  5482. var foreground = this.parent.getForeground();
  5483. if (!foreground) {
  5484. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5485. }
  5486. foreground.appendChild(dom.point);
  5487. }
  5488. this.displayed = true;
  5489. // update contents
  5490. if (this.data.content != this.content) {
  5491. this.content = this.data.content;
  5492. if (this.content instanceof Element) {
  5493. dom.content.innerHTML = '';
  5494. dom.content.appendChild(this.content);
  5495. }
  5496. else if (this.data.content != undefined) {
  5497. dom.content.innerHTML = this.content;
  5498. }
  5499. else {
  5500. throw new Error('Property "content" missing in item ' + this.data.id);
  5501. }
  5502. this.dirty = true;
  5503. }
  5504. // update class
  5505. var className = (this.data.className? ' ' + this.data.className : '') +
  5506. (this.selected ? ' selected' : '');
  5507. if (this.className != className) {
  5508. this.className = className;
  5509. dom.point.className = 'item point' + className;
  5510. dom.dot.className = 'item dot' + className;
  5511. this.dirty = true;
  5512. }
  5513. // recalculate size
  5514. if (this.dirty) {
  5515. this.width = dom.point.offsetWidth;
  5516. this.height = dom.point.offsetHeight;
  5517. this.props.dot.width = dom.dot.offsetWidth;
  5518. this.props.dot.height = dom.dot.offsetHeight;
  5519. this.props.content.height = dom.content.offsetHeight;
  5520. // resize contents
  5521. dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
  5522. //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
  5523. dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
  5524. dom.dot.style.left = (this.props.dot.width / 2) + 'px';
  5525. this.dirty = false;
  5526. }
  5527. this._repaintDeleteButton(dom.point);
  5528. };
  5529. /**
  5530. * Show the item in the DOM (when not already visible). The items DOM will
  5531. * be created when needed.
  5532. */
  5533. ItemPoint.prototype.show = function show() {
  5534. if (!this.displayed) {
  5535. this.repaint();
  5536. }
  5537. };
  5538. /**
  5539. * Hide the item from the DOM (when visible)
  5540. */
  5541. ItemPoint.prototype.hide = function hide() {
  5542. if (this.displayed) {
  5543. if (this.dom.point.parentNode) {
  5544. this.dom.point.parentNode.removeChild(this.dom.point);
  5545. }
  5546. this.top = null;
  5547. this.left = null;
  5548. this.displayed = false;
  5549. }
  5550. };
  5551. /**
  5552. * Reposition the item horizontally
  5553. * @Override
  5554. */
  5555. ItemPoint.prototype.repositionX = function repositionX() {
  5556. var start = this.defaultOptions.toScreen(this.data.start);
  5557. this.left = start - this.props.dot.width;
  5558. // reposition point
  5559. this.dom.point.style.left = this.left + 'px';
  5560. };
  5561. /**
  5562. * Reposition the item vertically
  5563. * @Override
  5564. */
  5565. ItemPoint.prototype.repositionY = function repositionY () {
  5566. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5567. point = this.dom.point;
  5568. if (orientation == 'top') {
  5569. point.style.top = this.top + 'px';
  5570. point.style.bottom = '';
  5571. }
  5572. else {
  5573. point.style.top = '';
  5574. point.style.bottom = this.top + 'px';
  5575. }
  5576. };
  5577. /**
  5578. * @constructor ItemRange
  5579. * @extends Item
  5580. * @param {Object} data Object containing parameters start, end
  5581. * content, className.
  5582. * @param {Object} [options] Options to set initial property values
  5583. * @param {Object} [defaultOptions] default options
  5584. * // TODO: describe available options
  5585. */
  5586. function ItemRange (data, options, defaultOptions) {
  5587. this.props = {
  5588. content: {
  5589. width: 0
  5590. }
  5591. };
  5592. // validate data
  5593. if (data) {
  5594. if (data.start == undefined) {
  5595. throw new Error('Property "start" missing in item ' + data.id);
  5596. }
  5597. if (data.end == undefined) {
  5598. throw new Error('Property "end" missing in item ' + data.id);
  5599. }
  5600. }
  5601. Item.call(this, data, options, defaultOptions);
  5602. }
  5603. ItemRange.prototype = new Item (null);
  5604. ItemRange.prototype.baseClassName = 'item range';
  5605. /**
  5606. * Check whether this item is visible inside given range
  5607. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5608. * @returns {boolean} True if visible
  5609. */
  5610. ItemRange.prototype.isVisible = function isVisible (range) {
  5611. // determine visibility
  5612. return (this.data.start < range.end) && (this.data.end > range.start);
  5613. };
  5614. /**
  5615. * Repaint the item
  5616. */
  5617. ItemRange.prototype.repaint = function repaint() {
  5618. var dom = this.dom;
  5619. if (!dom) {
  5620. // create DOM
  5621. this.dom = {};
  5622. dom = this.dom;
  5623. // background box
  5624. dom.box = document.createElement('div');
  5625. // className is updated in repaint()
  5626. // contents box
  5627. dom.content = document.createElement('div');
  5628. dom.content.className = 'content';
  5629. dom.box.appendChild(dom.content);
  5630. // attach this item as attribute
  5631. dom.box['timeline-item'] = this;
  5632. }
  5633. // append DOM to parent DOM
  5634. if (!this.parent) {
  5635. throw new Error('Cannot repaint item: no parent attached');
  5636. }
  5637. if (!dom.box.parentNode) {
  5638. var foreground = this.parent.getForeground();
  5639. if (!foreground) {
  5640. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5641. }
  5642. foreground.appendChild(dom.box);
  5643. }
  5644. this.displayed = true;
  5645. // update contents
  5646. if (this.data.content != this.content) {
  5647. this.content = this.data.content;
  5648. if (this.content instanceof Element) {
  5649. dom.content.innerHTML = '';
  5650. dom.content.appendChild(this.content);
  5651. }
  5652. else if (this.data.content != undefined) {
  5653. dom.content.innerHTML = this.content;
  5654. }
  5655. else {
  5656. throw new Error('Property "content" missing in item ' + this.data.id);
  5657. }
  5658. this.dirty = true;
  5659. }
  5660. // update class
  5661. var className = (this.data.className ? (' ' + this.data.className) : '') +
  5662. (this.selected ? ' selected' : '');
  5663. if (this.className != className) {
  5664. this.className = className;
  5665. dom.box.className = this.baseClassName + className;
  5666. this.dirty = true;
  5667. }
  5668. // recalculate size
  5669. if (this.dirty) {
  5670. this.props.content.width = this.dom.content.offsetWidth;
  5671. this.height = this.dom.box.offsetHeight;
  5672. this.dirty = false;
  5673. }
  5674. this._repaintDeleteButton(dom.box);
  5675. this._repaintDragLeft();
  5676. this._repaintDragRight();
  5677. };
  5678. /**
  5679. * Show the item in the DOM (when not already visible). The items DOM will
  5680. * be created when needed.
  5681. */
  5682. ItemRange.prototype.show = function show() {
  5683. if (!this.displayed) {
  5684. this.repaint();
  5685. }
  5686. };
  5687. /**
  5688. * Hide the item from the DOM (when visible)
  5689. * @return {Boolean} changed
  5690. */
  5691. ItemRange.prototype.hide = function hide() {
  5692. if (this.displayed) {
  5693. var box = this.dom.box;
  5694. if (box.parentNode) {
  5695. box.parentNode.removeChild(box);
  5696. }
  5697. this.top = null;
  5698. this.left = null;
  5699. this.displayed = false;
  5700. }
  5701. };
  5702. /**
  5703. * Reposition the item horizontally
  5704. * @Override
  5705. */
  5706. ItemRange.prototype.repositionX = function repositionX() {
  5707. var props = this.props,
  5708. parentWidth = this.parent.width,
  5709. start = this.defaultOptions.toScreen(this.data.start),
  5710. end = this.defaultOptions.toScreen(this.data.end),
  5711. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5712. contentLeft;
  5713. // limit the width of the this, as browsers cannot draw very wide divs
  5714. if (start < -parentWidth) {
  5715. start = -parentWidth;
  5716. }
  5717. if (end > 2 * parentWidth) {
  5718. end = 2 * parentWidth;
  5719. }
  5720. // when range exceeds left of the window, position the contents at the left of the visible area
  5721. if (start < 0) {
  5722. contentLeft = Math.min(-start,
  5723. (end - start - props.content.width - 2 * padding));
  5724. // TODO: remove the need for options.padding. it's terrible.
  5725. }
  5726. else {
  5727. contentLeft = 0;
  5728. }
  5729. this.left = start;
  5730. this.width = Math.max(end - start, 1);
  5731. this.dom.box.style.left = this.left + 'px';
  5732. this.dom.box.style.width = this.width + 'px';
  5733. this.dom.content.style.left = contentLeft + 'px';
  5734. };
  5735. /**
  5736. * Reposition the item vertically
  5737. * @Override
  5738. */
  5739. ItemRange.prototype.repositionY = function repositionY() {
  5740. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5741. box = this.dom.box;
  5742. if (orientation == 'top') {
  5743. box.style.top = this.top + 'px';
  5744. box.style.bottom = '';
  5745. }
  5746. else {
  5747. box.style.top = '';
  5748. box.style.bottom = this.top + 'px';
  5749. }
  5750. };
  5751. /**
  5752. * Repaint a drag area on the left side of the range when the range is selected
  5753. * @protected
  5754. */
  5755. ItemRange.prototype._repaintDragLeft = function () {
  5756. if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
  5757. // create and show drag area
  5758. var dragLeft = document.createElement('div');
  5759. dragLeft.className = 'drag-left';
  5760. dragLeft.dragLeftItem = this;
  5761. // TODO: this should be redundant?
  5762. Hammer(dragLeft, {
  5763. preventDefault: true
  5764. }).on('drag', function () {
  5765. //console.log('drag left')
  5766. });
  5767. this.dom.box.appendChild(dragLeft);
  5768. this.dom.dragLeft = dragLeft;
  5769. }
  5770. else if (!this.selected && this.dom.dragLeft) {
  5771. // delete drag area
  5772. if (this.dom.dragLeft.parentNode) {
  5773. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  5774. }
  5775. this.dom.dragLeft = null;
  5776. }
  5777. };
  5778. /**
  5779. * Repaint a drag area on the right side of the range when the range is selected
  5780. * @protected
  5781. */
  5782. ItemRange.prototype._repaintDragRight = function () {
  5783. if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
  5784. // create and show drag area
  5785. var dragRight = document.createElement('div');
  5786. dragRight.className = 'drag-right';
  5787. dragRight.dragRightItem = this;
  5788. // TODO: this should be redundant?
  5789. Hammer(dragRight, {
  5790. preventDefault: true
  5791. }).on('drag', function () {
  5792. //console.log('drag right')
  5793. });
  5794. this.dom.box.appendChild(dragRight);
  5795. this.dom.dragRight = dragRight;
  5796. }
  5797. else if (!this.selected && this.dom.dragRight) {
  5798. // delete drag area
  5799. if (this.dom.dragRight.parentNode) {
  5800. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  5801. }
  5802. this.dom.dragRight = null;
  5803. }
  5804. };
  5805. /**
  5806. * @constructor ItemRangeOverflow
  5807. * @extends ItemRange
  5808. * @param {Object} data Object containing parameters start, end
  5809. * content, className.
  5810. * @param {Object} [options] Options to set initial property values
  5811. * @param {Object} [defaultOptions] default options
  5812. * // TODO: describe available options
  5813. */
  5814. function ItemRangeOverflow (data, options, defaultOptions) {
  5815. this.props = {
  5816. content: {
  5817. left: 0,
  5818. width: 0
  5819. }
  5820. };
  5821. ItemRange.call(this, data, options, defaultOptions);
  5822. }
  5823. ItemRangeOverflow.prototype = new ItemRange (null);
  5824. ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
  5825. /**
  5826. * Reposition the item horizontally
  5827. * @Override
  5828. */
  5829. ItemRangeOverflow.prototype.repositionX = function repositionX() {
  5830. var parentWidth = this.parent.width,
  5831. start = this.defaultOptions.toScreen(this.data.start),
  5832. end = this.defaultOptions.toScreen(this.data.end),
  5833. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5834. contentLeft;
  5835. // limit the width of the this, as browsers cannot draw very wide divs
  5836. if (start < -parentWidth) {
  5837. start = -parentWidth;
  5838. }
  5839. if (end > 2 * parentWidth) {
  5840. end = 2 * parentWidth;
  5841. }
  5842. // when range exceeds left of the window, position the contents at the left of the visible area
  5843. contentLeft = Math.max(-start, 0);
  5844. this.left = start;
  5845. var boxWidth = Math.max(end - start, 1);
  5846. this.width = boxWidth + this.props.content.width;
  5847. // Note: The calculation of width is an optimistic calculation, giving
  5848. // a width which will not change when moving the Timeline
  5849. // So no restacking needed, which is nicer for the eye
  5850. this.dom.box.style.left = this.left + 'px';
  5851. this.dom.box.style.width = boxWidth + 'px';
  5852. this.dom.content.style.left = contentLeft + 'px';
  5853. };
  5854. /**
  5855. * @constructor Group
  5856. * @param {Number | String} groupId
  5857. * @param {Object} data
  5858. * @param {ItemSet} itemSet
  5859. */
  5860. function Group (groupId, data, itemSet) {
  5861. this.groupId = groupId;
  5862. this.itemSet = itemSet;
  5863. this.dom = {};
  5864. this.props = {
  5865. label: {
  5866. width: 0,
  5867. height: 0
  5868. }
  5869. };
  5870. this.items = {}; // items filtered by groupId of this group
  5871. this.visibleItems = []; // items currently visible in window
  5872. this.orderedItems = { // items sorted by start and by end
  5873. byStart: [],
  5874. byEnd: []
  5875. };
  5876. this._create();
  5877. this.setData(data);
  5878. }
  5879. /**
  5880. * Create DOM elements for the group
  5881. * @private
  5882. */
  5883. Group.prototype._create = function() {
  5884. var label = document.createElement('div');
  5885. label.className = 'vlabel';
  5886. this.dom.label = label;
  5887. var inner = document.createElement('div');
  5888. inner.className = 'inner';
  5889. label.appendChild(inner);
  5890. this.dom.inner = inner;
  5891. var foreground = document.createElement('div');
  5892. foreground.className = 'group';
  5893. foreground['timeline-group'] = this;
  5894. this.dom.foreground = foreground;
  5895. this.dom.background = document.createElement('div');
  5896. this.dom.axis = document.createElement('div');
  5897. };
  5898. /**
  5899. * Set the group data for this group
  5900. * @param {Object} data Group data, can contain properties content and className
  5901. */
  5902. Group.prototype.setData = function setData(data) {
  5903. // update contents
  5904. var content = data && data.content;
  5905. if (content instanceof Element) {
  5906. this.dom.inner.appendChild(content);
  5907. }
  5908. else if (content != undefined) {
  5909. this.dom.inner.innerHTML = content;
  5910. }
  5911. else {
  5912. this.dom.inner.innerHTML = this.groupId;
  5913. }
  5914. // update className
  5915. var className = data && data.className;
  5916. if (className) {
  5917. util.addClassName(this.dom.label, className);
  5918. }
  5919. };
  5920. /**
  5921. * Get the foreground container element
  5922. * @return {HTMLElement} foreground
  5923. */
  5924. Group.prototype.getForeground = function getForeground() {
  5925. return this.dom.foreground;
  5926. };
  5927. /**
  5928. * Get the background container element
  5929. * @return {HTMLElement} background
  5930. */
  5931. Group.prototype.getBackground = function getBackground() {
  5932. return this.dom.background;
  5933. };
  5934. /**
  5935. * Get the axis container element
  5936. * @return {HTMLElement} axis
  5937. */
  5938. Group.prototype.getAxis = function getAxis() {
  5939. return this.dom.axis;
  5940. };
  5941. /**
  5942. * Get the width of the group label
  5943. * @return {number} width
  5944. */
  5945. Group.prototype.getLabelWidth = function getLabelWidth() {
  5946. return this.props.label.width;
  5947. };
  5948. /**
  5949. * Repaint this group
  5950. * @param {{start: number, end: number}} range
  5951. * @param {{item: number, axis: number}} margin
  5952. * @param {boolean} [restack=false] Force restacking of all items
  5953. * @return {boolean} Returns true if the group is resized
  5954. */
  5955. Group.prototype.repaint = function repaint(range, margin, restack) {
  5956. var resized = false;
  5957. this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
  5958. // reposition visible items vertically
  5959. if (this.itemSet.options.stack) { // TODO: ugly way to access options...
  5960. stack.stack(this.visibleItems, margin, restack);
  5961. }
  5962. else { // no stacking
  5963. stack.nostack(this.visibleItems, margin);
  5964. }
  5965. this.stackDirty = false;
  5966. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  5967. var item = this.visibleItems[i];
  5968. item.repositionY();
  5969. }
  5970. // recalculate the height of the group
  5971. var height;
  5972. var visibleItems = this.visibleItems;
  5973. if (visibleItems.length) {
  5974. var min = visibleItems[0].top;
  5975. var max = visibleItems[0].top + visibleItems[0].height;
  5976. util.forEach(visibleItems, function (item) {
  5977. min = Math.min(min, item.top);
  5978. max = Math.max(max, (item.top + item.height));
  5979. });
  5980. height = (max - min) + margin.axis + margin.item;
  5981. }
  5982. else {
  5983. height = margin.axis + margin.item;
  5984. }
  5985. height = Math.max(height, this.props.label.height);
  5986. // calculate actual size and position
  5987. var foreground = this.dom.foreground;
  5988. this.top = foreground.offsetTop;
  5989. this.left = foreground.offsetLeft;
  5990. this.width = foreground.offsetWidth;
  5991. resized = util.updateProperty(this, 'height', height) || resized;
  5992. // recalculate size of label
  5993. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  5994. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  5995. // apply new height
  5996. foreground.style.height = height + 'px';
  5997. this.dom.label.style.height = height + 'px';
  5998. return resized;
  5999. };
  6000. /**
  6001. * Show this group: attach to the DOM
  6002. */
  6003. Group.prototype.show = function show() {
  6004. if (!this.dom.label.parentNode) {
  6005. this.itemSet.getLabelSet().appendChild(this.dom.label);
  6006. }
  6007. if (!this.dom.foreground.parentNode) {
  6008. this.itemSet.getForeground().appendChild(this.dom.foreground);
  6009. }
  6010. if (!this.dom.background.parentNode) {
  6011. this.itemSet.getBackground().appendChild(this.dom.background);
  6012. }
  6013. if (!this.dom.axis.parentNode) {
  6014. this.itemSet.getAxis().appendChild(this.dom.axis);
  6015. }
  6016. };
  6017. /**
  6018. * Hide this group: remove from the DOM
  6019. */
  6020. Group.prototype.hide = function hide() {
  6021. var label = this.dom.label;
  6022. if (label.parentNode) {
  6023. label.parentNode.removeChild(label);
  6024. }
  6025. var foreground = this.dom.foreground;
  6026. if (foreground.parentNode) {
  6027. foreground.parentNode.removeChild(foreground);
  6028. }
  6029. var background = this.dom.background;
  6030. if (background.parentNode) {
  6031. background.parentNode.removeChild(background);
  6032. }
  6033. var axis = this.dom.axis;
  6034. if (axis.parentNode) {
  6035. axis.parentNode.removeChild(axis);
  6036. }
  6037. };
  6038. /**
  6039. * Add an item to the group
  6040. * @param {Item} item
  6041. */
  6042. Group.prototype.add = function add(item) {
  6043. this.items[item.id] = item;
  6044. item.setParent(this);
  6045. if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
  6046. var range = this.itemSet.range; // TODO: not nice accessing the range like this
  6047. this._checkIfVisible(item, this.visibleItems, range);
  6048. }
  6049. };
  6050. /**
  6051. * Remove an item from the group
  6052. * @param {Item} item
  6053. */
  6054. Group.prototype.remove = function remove(item) {
  6055. delete this.items[item.id];
  6056. item.setParent(this.itemSet);
  6057. // remove from visible items
  6058. var index = this.visibleItems.indexOf(item);
  6059. if (index != -1) this.visibleItems.splice(index, 1);
  6060. // TODO: also remove from ordered items?
  6061. };
  6062. /**
  6063. * Remove an item from the corresponding DataSet
  6064. * @param {Item} item
  6065. */
  6066. Group.prototype.removeFromDataSet = function removeFromDataSet(item) {
  6067. this.itemSet.removeItem(item.id);
  6068. };
  6069. /**
  6070. * Reorder the items
  6071. */
  6072. Group.prototype.order = function order() {
  6073. var array = util.toArray(this.items);
  6074. this.orderedItems.byStart = array;
  6075. this.orderedItems.byEnd = this._constructByEndArray(array);
  6076. stack.orderByStart(this.orderedItems.byStart);
  6077. stack.orderByEnd(this.orderedItems.byEnd);
  6078. };
  6079. /**
  6080. * Create an array containing all items being a range (having an end date)
  6081. * @param {Item[]} array
  6082. * @returns {ItemRange[]}
  6083. * @private
  6084. */
  6085. Group.prototype._constructByEndArray = function _constructByEndArray(array) {
  6086. var endArray = [];
  6087. for (var i = 0; i < array.length; i++) {
  6088. if (array[i] instanceof ItemRange) {
  6089. endArray.push(array[i]);
  6090. }
  6091. }
  6092. return endArray;
  6093. };
  6094. /**
  6095. * Update the visible items
  6096. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  6097. * @param {Item[]} visibleItems The previously visible items.
  6098. * @param {{start: number, end: number}} range Visible range
  6099. * @return {Item[]} visibleItems The new visible items.
  6100. * @private
  6101. */
  6102. Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
  6103. var initialPosByStart,
  6104. newVisibleItems = [],
  6105. i;
  6106. // first check if the items that were in view previously are still in view.
  6107. // this handles the case for the ItemRange that is both before and after the current one.
  6108. if (visibleItems.length > 0) {
  6109. for (i = 0; i < visibleItems.length; i++) {
  6110. this._checkIfVisible(visibleItems[i], newVisibleItems, range);
  6111. }
  6112. }
  6113. // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
  6114. if (newVisibleItems.length == 0) {
  6115. initialPosByStart = this._binarySearch(orderedItems, range, false);
  6116. }
  6117. else {
  6118. initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
  6119. }
  6120. // use visible search to find a visible ItemRange (only based on endTime)
  6121. var initialPosByEnd = this._binarySearch(orderedItems, range, true);
  6122. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  6123. if (initialPosByStart != -1) {
  6124. for (i = initialPosByStart; i >= 0; i--) {
  6125. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  6126. }
  6127. for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  6128. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  6129. }
  6130. }
  6131. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  6132. if (initialPosByEnd != -1) {
  6133. for (i = initialPosByEnd; i >= 0; i--) {
  6134. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  6135. }
  6136. for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  6137. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  6138. }
  6139. }
  6140. return newVisibleItems;
  6141. };
  6142. /**
  6143. * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
  6144. * arrays. This is done by giving a boolean value true if you want to use the byEnd.
  6145. * This is done to be able to select the correct if statement (we do not want to check if an item is visible, we want to check
  6146. * if the time we selected (start or end) is within the current range).
  6147. *
  6148. * The trick is that every interval has to either enter the screen at the initial load or by dragging. The case of the ItemRange that is
  6149. * before and after the current range is handled by simply checking if it was in view before and if it is again. For all the rest,
  6150. * either the start OR end time has to be in the range.
  6151. *
  6152. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems
  6153. * @param {{start: number, end: number}} range
  6154. * @param {Boolean} byEnd
  6155. * @returns {number}
  6156. * @private
  6157. */
  6158. Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
  6159. var array = [];
  6160. var byTime = byEnd ? 'end' : 'start';
  6161. if (byEnd == true) {array = orderedItems.byEnd; }
  6162. else {array = orderedItems.byStart;}
  6163. var interval = range.end - range.start;
  6164. var found = false;
  6165. var low = 0;
  6166. var high = array.length;
  6167. var guess = Math.floor(0.5*(high+low));
  6168. var newGuess;
  6169. if (high == 0) {guess = -1;}
  6170. else if (high == 1) {
  6171. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  6172. guess = 0;
  6173. }
  6174. else {
  6175. guess = -1;
  6176. }
  6177. }
  6178. else {
  6179. high -= 1;
  6180. while (found == false) {
  6181. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  6182. found = true;
  6183. }
  6184. else {
  6185. if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
  6186. low = Math.floor(0.5*(high+low));
  6187. }
  6188. else { // it is too big --> decrease high
  6189. high = Math.floor(0.5*(high+low));
  6190. }
  6191. newGuess = Math.floor(0.5*(high+low));
  6192. // not in list;
  6193. if (guess == newGuess) {
  6194. guess = -1;
  6195. found = true;
  6196. }
  6197. else {
  6198. guess = newGuess;
  6199. }
  6200. }
  6201. }
  6202. }
  6203. return guess;
  6204. };
  6205. /**
  6206. * this function checks if an item is invisible. If it is NOT we make it visible
  6207. * and add it to the global visible items. If it is, return true.
  6208. *
  6209. * @param {Item} item
  6210. * @param {Item[]} visibleItems
  6211. * @param {{start:number, end:number}} range
  6212. * @returns {boolean}
  6213. * @private
  6214. */
  6215. Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
  6216. if (item.isVisible(range)) {
  6217. if (!item.displayed) item.show();
  6218. item.repositionX();
  6219. if (visibleItems.indexOf(item) == -1) {
  6220. visibleItems.push(item);
  6221. }
  6222. return false;
  6223. }
  6224. else {
  6225. return true;
  6226. }
  6227. };
  6228. /**
  6229. * this function is very similar to the _checkIfInvisible() but it does not
  6230. * return booleans, hides the item if it should not be seen and always adds to
  6231. * the visibleItems.
  6232. * this one is for brute forcing and hiding.
  6233. *
  6234. * @param {Item} item
  6235. * @param {Array} visibleItems
  6236. * @param {{start:number, end:number}} range
  6237. * @private
  6238. */
  6239. Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
  6240. if (item.isVisible(range)) {
  6241. if (!item.displayed) item.show();
  6242. // reposition item horizontally
  6243. item.repositionX();
  6244. visibleItems.push(item);
  6245. }
  6246. else {
  6247. if (item.displayed) item.hide();
  6248. }
  6249. };
  6250. /**
  6251. * Create a timeline visualization
  6252. * @param {HTMLElement} container
  6253. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  6254. * @param {Object} [options] See Timeline.setOptions for the available options.
  6255. * @constructor
  6256. */
  6257. function Timeline (container, items, options) {
  6258. // validate arguments
  6259. if (!container) throw new Error('No container element provided');
  6260. var me = this;
  6261. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6262. this.options = {
  6263. orientation: 'bottom',
  6264. direction: 'horizontal', // 'horizontal' or 'vertical'
  6265. autoResize: true,
  6266. stack: true,
  6267. editable: {
  6268. updateTime: false,
  6269. updateGroup: false,
  6270. add: false,
  6271. remove: false
  6272. },
  6273. selectable: true,
  6274. snap: null, // will be specified after timeaxis is created
  6275. min: null,
  6276. max: null,
  6277. zoomMin: 10, // milliseconds
  6278. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6279. // moveable: true, // TODO: option moveable
  6280. // zoomable: true, // TODO: option zoomable
  6281. showMinorLabels: true,
  6282. showMajorLabels: true,
  6283. showCurrentTime: false,
  6284. showCustomTime: false,
  6285. type: 'box',
  6286. align: 'center',
  6287. margin: {
  6288. axis: 20,
  6289. item: 10
  6290. },
  6291. padding: 5,
  6292. onAdd: function (item, callback) {
  6293. callback(item);
  6294. },
  6295. onUpdate: function (item, callback) {
  6296. callback(item);
  6297. },
  6298. onMove: function (item, callback) {
  6299. callback(item);
  6300. },
  6301. onRemove: function (item, callback) {
  6302. callback(item);
  6303. },
  6304. toScreen: me._toScreen.bind(me),
  6305. toTime: me._toTime.bind(me)
  6306. };
  6307. // root panel
  6308. var rootOptions = util.extend(Object.create(this.options), {
  6309. height: function () {
  6310. if (me.options.height) {
  6311. // fixed height
  6312. return me.options.height;
  6313. }
  6314. else {
  6315. // auto height
  6316. // TODO: implement a css based solution to automatically have the right hight
  6317. return (me.timeAxis.height + me.contentPanel.height) + 'px';
  6318. }
  6319. }
  6320. });
  6321. this.rootPanel = new RootPanel(container, rootOptions);
  6322. // single select (or unselect) when tapping an item
  6323. this.rootPanel.on('tap', this._onSelectItem.bind(this));
  6324. // multi select when holding mouse/touch, or on ctrl+click
  6325. this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
  6326. // add item on doubletap
  6327. this.rootPanel.on('doubletap', this._onAddItem.bind(this));
  6328. // side panel
  6329. var sideOptions = util.extend(Object.create(this.options), {
  6330. top: function () {
  6331. return (sideOptions.orientation == 'top') ? '0' : '';
  6332. },
  6333. bottom: function () {
  6334. return (sideOptions.orientation == 'top') ? '' : '0';
  6335. },
  6336. left: '0',
  6337. right: null,
  6338. height: '100%',
  6339. width: function () {
  6340. if (me.itemSet) {
  6341. return me.itemSet.getLabelsWidth();
  6342. }
  6343. else {
  6344. return 0;
  6345. }
  6346. },
  6347. className: function () {
  6348. return 'side' + (me.groupsData ? '' : ' hidden');
  6349. }
  6350. });
  6351. this.sidePanel = new Panel(sideOptions);
  6352. this.rootPanel.appendChild(this.sidePanel);
  6353. // main panel (contains time axis and itemsets)
  6354. var mainOptions = util.extend(Object.create(this.options), {
  6355. left: function () {
  6356. // we align left to enable a smooth resizing of the window
  6357. return me.sidePanel.width;
  6358. },
  6359. right: null,
  6360. height: '100%',
  6361. width: function () {
  6362. return me.rootPanel.width - me.sidePanel.width;
  6363. },
  6364. className: 'main'
  6365. });
  6366. this.mainPanel = new Panel(mainOptions);
  6367. this.rootPanel.appendChild(this.mainPanel);
  6368. // range
  6369. // TODO: move range inside rootPanel?
  6370. var rangeOptions = Object.create(this.options);
  6371. this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
  6372. this.range.setRange(
  6373. now.clone().add('days', -3).valueOf(),
  6374. now.clone().add('days', 4).valueOf()
  6375. );
  6376. this.range.on('rangechange', function (properties) {
  6377. me.rootPanel.repaint();
  6378. me.emit('rangechange', properties);
  6379. });
  6380. this.range.on('rangechanged', function (properties) {
  6381. me.rootPanel.repaint();
  6382. me.emit('rangechanged', properties);
  6383. });
  6384. // panel with time axis
  6385. var timeAxisOptions = util.extend(Object.create(rootOptions), {
  6386. range: this.range,
  6387. left: null,
  6388. top: null,
  6389. width: null,
  6390. height: null
  6391. });
  6392. this.timeAxis = new TimeAxis(timeAxisOptions);
  6393. this.timeAxis.setRange(this.range);
  6394. this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
  6395. this.mainPanel.appendChild(this.timeAxis);
  6396. // content panel (contains itemset(s))
  6397. var contentOptions = util.extend(Object.create(this.options), {
  6398. top: function () {
  6399. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6400. },
  6401. bottom: function () {
  6402. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6403. },
  6404. left: null,
  6405. right: null,
  6406. height: null,
  6407. width: null,
  6408. className: 'content'
  6409. });
  6410. this.contentPanel = new Panel(contentOptions);
  6411. this.mainPanel.appendChild(this.contentPanel);
  6412. // content panel (contains the vertical lines of box items)
  6413. var backgroundOptions = util.extend(Object.create(this.options), {
  6414. top: function () {
  6415. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6416. },
  6417. bottom: function () {
  6418. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6419. },
  6420. left: null,
  6421. right: null,
  6422. height: function () {
  6423. return me.contentPanel.height;
  6424. },
  6425. width: null,
  6426. className: 'background'
  6427. });
  6428. this.backgroundPanel = new Panel(backgroundOptions);
  6429. this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
  6430. // panel with axis holding the dots of item boxes
  6431. var axisPanelOptions = util.extend(Object.create(rootOptions), {
  6432. left: 0,
  6433. top: function () {
  6434. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6435. },
  6436. bottom: function () {
  6437. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6438. },
  6439. width: '100%',
  6440. height: 0,
  6441. className: 'axis'
  6442. });
  6443. this.axisPanel = new Panel(axisPanelOptions);
  6444. this.mainPanel.appendChild(this.axisPanel);
  6445. // content panel (contains itemset(s))
  6446. var sideContentOptions = util.extend(Object.create(this.options), {
  6447. top: function () {
  6448. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6449. },
  6450. bottom: function () {
  6451. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6452. },
  6453. left: null,
  6454. right: null,
  6455. height: null,
  6456. width: null,
  6457. className: 'side-content'
  6458. });
  6459. this.sideContentPanel = new Panel(sideContentOptions);
  6460. this.sidePanel.appendChild(this.sideContentPanel);
  6461. // current time bar
  6462. // Note: time bar will be attached in this.setOptions when selected
  6463. this.currentTime = new CurrentTime(this.range, rootOptions);
  6464. // custom time bar
  6465. // Note: time bar will be attached in this.setOptions when selected
  6466. this.customTime = new CustomTime(rootOptions);
  6467. this.customTime.on('timechange', function (time) {
  6468. me.emit('timechange', time);
  6469. });
  6470. this.customTime.on('timechanged', function (time) {
  6471. me.emit('timechanged', time);
  6472. });
  6473. // itemset containing items and groups
  6474. var itemOptions = util.extend(Object.create(this.options), {
  6475. left: null,
  6476. right: null,
  6477. top: null,
  6478. bottom: null,
  6479. width: null,
  6480. height: null
  6481. });
  6482. this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
  6483. this.itemSet.setRange(this.range);
  6484. this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
  6485. this.contentPanel.appendChild(this.itemSet);
  6486. this.itemsData = null; // DataSet
  6487. this.groupsData = null; // DataSet
  6488. // apply options
  6489. if (options) {
  6490. this.setOptions(options);
  6491. }
  6492. // create itemset
  6493. if (items) {
  6494. this.setItems(items);
  6495. }
  6496. }
  6497. // turn Timeline into an event emitter
  6498. Emitter(Timeline.prototype);
  6499. /**
  6500. * Set options
  6501. * @param {Object} options TODO: describe the available options
  6502. */
  6503. Timeline.prototype.setOptions = function (options) {
  6504. util.extend(this.options, options);
  6505. if ('editable' in options) {
  6506. var isBoolean = typeof options.editable === 'boolean';
  6507. this.options.editable = {
  6508. updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
  6509. updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
  6510. add: isBoolean ? options.editable : (options.editable.add || false),
  6511. remove: isBoolean ? options.editable : (options.editable.remove || false)
  6512. };
  6513. }
  6514. // force update of range (apply new min/max etc.)
  6515. // both start and end are optional
  6516. this.range.setRange(options.start, options.end);
  6517. if ('editable' in options || 'selectable' in options) {
  6518. if (this.options.selectable) {
  6519. // force update of selection
  6520. this.setSelection(this.getSelection());
  6521. }
  6522. else {
  6523. // remove selection
  6524. this.setSelection([]);
  6525. }
  6526. }
  6527. // force the itemSet to refresh: options like orientation and margins may be changed
  6528. this.itemSet.markDirty();
  6529. // validate the callback functions
  6530. var validateCallback = (function (fn) {
  6531. if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
  6532. throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
  6533. }
  6534. }).bind(this);
  6535. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
  6536. // add/remove the current time bar
  6537. if (this.options.showCurrentTime) {
  6538. if (!this.mainPanel.hasChild(this.currentTime)) {
  6539. this.mainPanel.appendChild(this.currentTime);
  6540. this.currentTime.start();
  6541. }
  6542. }
  6543. else {
  6544. if (this.mainPanel.hasChild(this.currentTime)) {
  6545. this.currentTime.stop();
  6546. this.mainPanel.removeChild(this.currentTime);
  6547. }
  6548. }
  6549. // add/remove the custom time bar
  6550. if (this.options.showCustomTime) {
  6551. if (!this.mainPanel.hasChild(this.customTime)) {
  6552. this.mainPanel.appendChild(this.customTime);
  6553. }
  6554. }
  6555. else {
  6556. if (this.mainPanel.hasChild(this.customTime)) {
  6557. this.mainPanel.removeChild(this.customTime);
  6558. }
  6559. }
  6560. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  6561. if (options && options.order) {
  6562. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  6563. }
  6564. // repaint everything
  6565. this.rootPanel.repaint();
  6566. };
  6567. /**
  6568. * Set a custom time bar
  6569. * @param {Date} time
  6570. */
  6571. Timeline.prototype.setCustomTime = function (time) {
  6572. if (!this.customTime) {
  6573. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6574. }
  6575. this.customTime.setCustomTime(time);
  6576. };
  6577. /**
  6578. * Retrieve the current custom time.
  6579. * @return {Date} customTime
  6580. */
  6581. Timeline.prototype.getCustomTime = function() {
  6582. if (!this.customTime) {
  6583. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6584. }
  6585. return this.customTime.getCustomTime();
  6586. };
  6587. /**
  6588. * Set items
  6589. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  6590. */
  6591. Timeline.prototype.setItems = function(items) {
  6592. var initialLoad = (this.itemsData == null);
  6593. // convert to type DataSet when needed
  6594. var newDataSet;
  6595. if (!items) {
  6596. newDataSet = null;
  6597. }
  6598. else if (items instanceof DataSet || items instanceof DataView) {
  6599. newDataSet = items;
  6600. }
  6601. else {
  6602. // turn an array into a dataset
  6603. newDataSet = new DataSet(items, {
  6604. convert: {
  6605. start: 'Date',
  6606. end: 'Date'
  6607. }
  6608. });
  6609. }
  6610. // set items
  6611. this.itemsData = newDataSet;
  6612. this.itemSet.setItems(newDataSet);
  6613. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6614. this.fit();
  6615. var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
  6616. var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
  6617. this.setWindow(start, end);
  6618. }
  6619. };
  6620. /**
  6621. * Set groups
  6622. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  6623. */
  6624. Timeline.prototype.setGroups = function setGroups(groups) {
  6625. // convert to type DataSet when needed
  6626. var newDataSet;
  6627. if (!groups) {
  6628. newDataSet = null;
  6629. }
  6630. else if (groups instanceof DataSet || groups instanceof DataView) {
  6631. newDataSet = groups;
  6632. }
  6633. else {
  6634. // turn an array into a dataset
  6635. newDataSet = new DataSet(groups);
  6636. }
  6637. this.groupsData = newDataSet;
  6638. this.itemSet.setGroups(newDataSet);
  6639. };
  6640. /**
  6641. * Set Timeline window such that it fits all items
  6642. */
  6643. Timeline.prototype.fit = function fit() {
  6644. // apply the data range as range
  6645. var dataRange = this.getItemRange();
  6646. // add 5% space on both sides
  6647. var start = dataRange.min;
  6648. var end = dataRange.max;
  6649. if (start != null && end != null) {
  6650. var interval = (end.valueOf() - start.valueOf());
  6651. if (interval <= 0) {
  6652. // prevent an empty interval
  6653. interval = 24 * 60 * 60 * 1000; // 1 day
  6654. }
  6655. start = new Date(start.valueOf() - interval * 0.05);
  6656. end = new Date(end.valueOf() + interval * 0.05);
  6657. }
  6658. // skip range set if there is no start and end date
  6659. if (start === null && end === null) {
  6660. return;
  6661. }
  6662. this.range.setRange(start, end);
  6663. };
  6664. /**
  6665. * Get the data range of the item set.
  6666. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6667. * When no minimum is found, min==null
  6668. * When no maximum is found, max==null
  6669. */
  6670. Timeline.prototype.getItemRange = function getItemRange() {
  6671. // calculate min from start filed
  6672. var itemsData = this.itemsData,
  6673. min = null,
  6674. max = null;
  6675. if (itemsData) {
  6676. // calculate the minimum value of the field 'start'
  6677. var minItem = itemsData.min('start');
  6678. min = minItem ? minItem.start.valueOf() : null;
  6679. // calculate maximum value of fields 'start' and 'end'
  6680. var maxStartItem = itemsData.max('start');
  6681. if (maxStartItem) {
  6682. max = maxStartItem.start.valueOf();
  6683. }
  6684. var maxEndItem = itemsData.max('end');
  6685. if (maxEndItem) {
  6686. if (max == null) {
  6687. max = maxEndItem.end.valueOf();
  6688. }
  6689. else {
  6690. max = Math.max(max, maxEndItem.end.valueOf());
  6691. }
  6692. }
  6693. }
  6694. return {
  6695. min: (min != null) ? new Date(min) : null,
  6696. max: (max != null) ? new Date(max) : null
  6697. };
  6698. };
  6699. /**
  6700. * Set selected items by their id. Replaces the current selection
  6701. * Unknown id's are silently ignored.
  6702. * @param {Array} [ids] An array with zero or more id's of the items to be
  6703. * selected. If ids is an empty array, all items will be
  6704. * unselected.
  6705. */
  6706. Timeline.prototype.setSelection = function setSelection (ids) {
  6707. this.itemSet.setSelection(ids);
  6708. };
  6709. /**
  6710. * Get the selected items by their id
  6711. * @return {Array} ids The ids of the selected items
  6712. */
  6713. Timeline.prototype.getSelection = function getSelection() {
  6714. return this.itemSet.getSelection();
  6715. };
  6716. /**
  6717. * Set the visible window. Both parameters are optional, you can change only
  6718. * start or only end. Syntax:
  6719. *
  6720. * TimeLine.setWindow(start, end)
  6721. * TimeLine.setWindow(range)
  6722. *
  6723. * Where start and end can be a Date, number, or string, and range is an
  6724. * object with properties start and end.
  6725. *
  6726. * @param {Date | Number | String} [start] Start date of visible window
  6727. * @param {Date | Number | String} [end] End date of visible window
  6728. */
  6729. Timeline.prototype.setWindow = function setWindow(start, end) {
  6730. if (arguments.length == 1) {
  6731. var range = arguments[0];
  6732. this.range.setRange(range.start, range.end);
  6733. }
  6734. else {
  6735. this.range.setRange(start, end);
  6736. }
  6737. };
  6738. /**
  6739. * Get the visible window
  6740. * @return {{start: Date, end: Date}} Visible range
  6741. */
  6742. Timeline.prototype.getWindow = function setWindow() {
  6743. var range = this.range.getRange();
  6744. return {
  6745. start: new Date(range.start),
  6746. end: new Date(range.end)
  6747. };
  6748. };
  6749. /**
  6750. * Handle selecting/deselecting an item when tapping it
  6751. * @param {Event} event
  6752. * @private
  6753. */
  6754. // TODO: move this function to ItemSet
  6755. Timeline.prototype._onSelectItem = function (event) {
  6756. if (!this.options.selectable) return;
  6757. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  6758. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  6759. if (ctrlKey || shiftKey) {
  6760. this._onMultiSelectItem(event);
  6761. return;
  6762. }
  6763. var oldSelection = this.getSelection();
  6764. var item = ItemSet.itemFromTarget(event);
  6765. var selection = item ? [item.id] : [];
  6766. this.setSelection(selection);
  6767. var newSelection = this.getSelection();
  6768. // if selection is changed, emit a select event
  6769. if (!util.equalArray(oldSelection, newSelection)) {
  6770. this.emit('select', {
  6771. items: this.getSelection()
  6772. });
  6773. }
  6774. event.stopPropagation();
  6775. };
  6776. /**
  6777. * Handle creation and updates of an item on double tap
  6778. * @param event
  6779. * @private
  6780. */
  6781. Timeline.prototype._onAddItem = function (event) {
  6782. if (!this.options.selectable) return;
  6783. if (!this.options.editable.add) return;
  6784. var me = this,
  6785. item = ItemSet.itemFromTarget(event);
  6786. if (item) {
  6787. // update item
  6788. // execute async handler to update the item (or cancel it)
  6789. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  6790. this.options.onUpdate(itemData, function (itemData) {
  6791. if (itemData) {
  6792. me.itemsData.update(itemData);
  6793. }
  6794. });
  6795. }
  6796. else {
  6797. // add item
  6798. var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
  6799. var x = event.gesture.center.pageX - xAbs;
  6800. var newItem = {
  6801. start: this.timeAxis.snap(this._toTime(x)),
  6802. content: 'new item'
  6803. };
  6804. var id = util.randomUUID();
  6805. newItem[this.itemsData.fieldId] = id;
  6806. var group = ItemSet.groupFromTarget(event);
  6807. if (group) {
  6808. newItem.group = group.groupId;
  6809. }
  6810. // execute async handler to customize (or cancel) adding an item
  6811. this.options.onAdd(newItem, function (item) {
  6812. if (item) {
  6813. me.itemsData.add(newItem);
  6814. // TODO: need to trigger a repaint?
  6815. }
  6816. });
  6817. }
  6818. };
  6819. /**
  6820. * Handle selecting/deselecting multiple items when holding an item
  6821. * @param {Event} event
  6822. * @private
  6823. */
  6824. // TODO: move this function to ItemSet
  6825. Timeline.prototype._onMultiSelectItem = function (event) {
  6826. if (!this.options.selectable) return;
  6827. var selection,
  6828. item = ItemSet.itemFromTarget(event);
  6829. if (item) {
  6830. // multi select items
  6831. selection = this.getSelection(); // current selection
  6832. var index = selection.indexOf(item.id);
  6833. if (index == -1) {
  6834. // item is not yet selected -> select it
  6835. selection.push(item.id);
  6836. }
  6837. else {
  6838. // item is already selected -> deselect it
  6839. selection.splice(index, 1);
  6840. }
  6841. this.setSelection(selection);
  6842. this.emit('select', {
  6843. items: this.getSelection()
  6844. });
  6845. event.stopPropagation();
  6846. }
  6847. };
  6848. /**
  6849. * Convert a position on screen (pixels) to a datetime
  6850. * @param {int} x Position on the screen in pixels
  6851. * @return {Date} time The datetime the corresponds with given position x
  6852. * @private
  6853. */
  6854. Timeline.prototype._toTime = function _toTime(x) {
  6855. var conversion = this.range.conversion(this.mainPanel.width);
  6856. return new Date(x / conversion.scale + conversion.offset);
  6857. };
  6858. /**
  6859. * Convert a datetime (Date object) into a position on the screen
  6860. * @param {Date} time A date
  6861. * @return {int} x The position on the screen in pixels which corresponds
  6862. * with the given date.
  6863. * @private
  6864. */
  6865. Timeline.prototype._toScreen = function _toScreen(time) {
  6866. var conversion = this.range.conversion(this.mainPanel.width);
  6867. return (time.valueOf() - conversion.offset) * conversion.scale;
  6868. };
  6869. (function(exports) {
  6870. /**
  6871. * Parse a text source containing data in DOT language into a JSON object.
  6872. * The object contains two lists: one with nodes and one with edges.
  6873. *
  6874. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6875. *
  6876. * @param {String} data Text containing a graph in DOT-notation
  6877. * @return {Object} graph An object containing two parameters:
  6878. * {Object[]} nodes
  6879. * {Object[]} edges
  6880. */
  6881. function parseDOT (data) {
  6882. dot = data;
  6883. return parseGraph();
  6884. }
  6885. // token types enumeration
  6886. var TOKENTYPE = {
  6887. NULL : 0,
  6888. DELIMITER : 1,
  6889. IDENTIFIER: 2,
  6890. UNKNOWN : 3
  6891. };
  6892. // map with all delimiters
  6893. var DELIMITERS = {
  6894. '{': true,
  6895. '}': true,
  6896. '[': true,
  6897. ']': true,
  6898. ';': true,
  6899. '=': true,
  6900. ',': true,
  6901. '->': true,
  6902. '--': true
  6903. };
  6904. var dot = ''; // current dot file
  6905. var index = 0; // current index in dot file
  6906. var c = ''; // current token character in expr
  6907. var token = ''; // current token
  6908. var tokenType = TOKENTYPE.NULL; // type of the token
  6909. /**
  6910. * Get the first character from the dot file.
  6911. * The character is stored into the char c. If the end of the dot file is
  6912. * reached, the function puts an empty string in c.
  6913. */
  6914. function first() {
  6915. index = 0;
  6916. c = dot.charAt(0);
  6917. }
  6918. /**
  6919. * Get the next character from the dot file.
  6920. * The character is stored into the char c. If the end of the dot file is
  6921. * reached, the function puts an empty string in c.
  6922. */
  6923. function next() {
  6924. index++;
  6925. c = dot.charAt(index);
  6926. }
  6927. /**
  6928. * Preview the next character from the dot file.
  6929. * @return {String} cNext
  6930. */
  6931. function nextPreview() {
  6932. return dot.charAt(index + 1);
  6933. }
  6934. /**
  6935. * Test whether given character is alphabetic or numeric
  6936. * @param {String} c
  6937. * @return {Boolean} isAlphaNumeric
  6938. */
  6939. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6940. function isAlphaNumeric(c) {
  6941. return regexAlphaNumeric.test(c);
  6942. }
  6943. /**
  6944. * Merge all properties of object b into object b
  6945. * @param {Object} a
  6946. * @param {Object} b
  6947. * @return {Object} a
  6948. */
  6949. function merge (a, b) {
  6950. if (!a) {
  6951. a = {};
  6952. }
  6953. if (b) {
  6954. for (var name in b) {
  6955. if (b.hasOwnProperty(name)) {
  6956. a[name] = b[name];
  6957. }
  6958. }
  6959. }
  6960. return a;
  6961. }
  6962. /**
  6963. * Set a value in an object, where the provided parameter name can be a
  6964. * path with nested parameters. For example:
  6965. *
  6966. * var obj = {a: 2};
  6967. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6968. *
  6969. * @param {Object} obj
  6970. * @param {String} path A parameter name or dot-separated parameter path,
  6971. * like "color.highlight.border".
  6972. * @param {*} value
  6973. */
  6974. function setValue(obj, path, value) {
  6975. var keys = path.split('.');
  6976. var o = obj;
  6977. while (keys.length) {
  6978. var key = keys.shift();
  6979. if (keys.length) {
  6980. // this isn't the end point
  6981. if (!o[key]) {
  6982. o[key] = {};
  6983. }
  6984. o = o[key];
  6985. }
  6986. else {
  6987. // this is the end point
  6988. o[key] = value;
  6989. }
  6990. }
  6991. }
  6992. /**
  6993. * Add a node to a graph object. If there is already a node with
  6994. * the same id, their attributes will be merged.
  6995. * @param {Object} graph
  6996. * @param {Object} node
  6997. */
  6998. function addNode(graph, node) {
  6999. var i, len;
  7000. var current = null;
  7001. // find root graph (in case of subgraph)
  7002. var graphs = [graph]; // list with all graphs from current graph to root graph
  7003. var root = graph;
  7004. while (root.parent) {
  7005. graphs.push(root.parent);
  7006. root = root.parent;
  7007. }
  7008. // find existing node (at root level) by its id
  7009. if (root.nodes) {
  7010. for (i = 0, len = root.nodes.length; i < len; i++) {
  7011. if (node.id === root.nodes[i].id) {
  7012. current = root.nodes[i];
  7013. break;
  7014. }
  7015. }
  7016. }
  7017. if (!current) {
  7018. // this is a new node
  7019. current = {
  7020. id: node.id
  7021. };
  7022. if (graph.node) {
  7023. // clone default attributes
  7024. current.attr = merge(current.attr, graph.node);
  7025. }
  7026. }
  7027. // add node to this (sub)graph and all its parent graphs
  7028. for (i = graphs.length - 1; i >= 0; i--) {
  7029. var g = graphs[i];
  7030. if (!g.nodes) {
  7031. g.nodes = [];
  7032. }
  7033. if (g.nodes.indexOf(current) == -1) {
  7034. g.nodes.push(current);
  7035. }
  7036. }
  7037. // merge attributes
  7038. if (node.attr) {
  7039. current.attr = merge(current.attr, node.attr);
  7040. }
  7041. }
  7042. /**
  7043. * Add an edge to a graph object
  7044. * @param {Object} graph
  7045. * @param {Object} edge
  7046. */
  7047. function addEdge(graph, edge) {
  7048. if (!graph.edges) {
  7049. graph.edges = [];
  7050. }
  7051. graph.edges.push(edge);
  7052. if (graph.edge) {
  7053. var attr = merge({}, graph.edge); // clone default attributes
  7054. edge.attr = merge(attr, edge.attr); // merge attributes
  7055. }
  7056. }
  7057. /**
  7058. * Create an edge to a graph object
  7059. * @param {Object} graph
  7060. * @param {String | Number | Object} from
  7061. * @param {String | Number | Object} to
  7062. * @param {String} type
  7063. * @param {Object | null} attr
  7064. * @return {Object} edge
  7065. */
  7066. function createEdge(graph, from, to, type, attr) {
  7067. var edge = {
  7068. from: from,
  7069. to: to,
  7070. type: type
  7071. };
  7072. if (graph.edge) {
  7073. edge.attr = merge({}, graph.edge); // clone default attributes
  7074. }
  7075. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  7076. return edge;
  7077. }
  7078. /**
  7079. * Get next token in the current dot file.
  7080. * The token and token type are available as token and tokenType
  7081. */
  7082. function getToken() {
  7083. tokenType = TOKENTYPE.NULL;
  7084. token = '';
  7085. // skip over whitespaces
  7086. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7087. next();
  7088. }
  7089. do {
  7090. var isComment = false;
  7091. // skip comment
  7092. if (c == '#') {
  7093. // find the previous non-space character
  7094. var i = index - 1;
  7095. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7096. i--;
  7097. }
  7098. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7099. // the # is at the start of a line, this is indeed a line comment
  7100. while (c != '' && c != '\n') {
  7101. next();
  7102. }
  7103. isComment = true;
  7104. }
  7105. }
  7106. if (c == '/' && nextPreview() == '/') {
  7107. // skip line comment
  7108. while (c != '' && c != '\n') {
  7109. next();
  7110. }
  7111. isComment = true;
  7112. }
  7113. if (c == '/' && nextPreview() == '*') {
  7114. // skip block comment
  7115. while (c != '') {
  7116. if (c == '*' && nextPreview() == '/') {
  7117. // end of block comment found. skip these last two characters
  7118. next();
  7119. next();
  7120. break;
  7121. }
  7122. else {
  7123. next();
  7124. }
  7125. }
  7126. isComment = true;
  7127. }
  7128. // skip over whitespaces
  7129. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7130. next();
  7131. }
  7132. }
  7133. while (isComment);
  7134. // check for end of dot file
  7135. if (c == '') {
  7136. // token is still empty
  7137. tokenType = TOKENTYPE.DELIMITER;
  7138. return;
  7139. }
  7140. // check for delimiters consisting of 2 characters
  7141. var c2 = c + nextPreview();
  7142. if (DELIMITERS[c2]) {
  7143. tokenType = TOKENTYPE.DELIMITER;
  7144. token = c2;
  7145. next();
  7146. next();
  7147. return;
  7148. }
  7149. // check for delimiters consisting of 1 character
  7150. if (DELIMITERS[c]) {
  7151. tokenType = TOKENTYPE.DELIMITER;
  7152. token = c;
  7153. next();
  7154. return;
  7155. }
  7156. // check for an identifier (number or string)
  7157. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7158. if (isAlphaNumeric(c) || c == '-') {
  7159. token += c;
  7160. next();
  7161. while (isAlphaNumeric(c)) {
  7162. token += c;
  7163. next();
  7164. }
  7165. if (token == 'false') {
  7166. token = false; // convert to boolean
  7167. }
  7168. else if (token == 'true') {
  7169. token = true; // convert to boolean
  7170. }
  7171. else if (!isNaN(Number(token))) {
  7172. token = Number(token); // convert to number
  7173. }
  7174. tokenType = TOKENTYPE.IDENTIFIER;
  7175. return;
  7176. }
  7177. // check for a string enclosed by double quotes
  7178. if (c == '"') {
  7179. next();
  7180. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7181. token += c;
  7182. if (c == '"') { // skip the escape character
  7183. next();
  7184. }
  7185. next();
  7186. }
  7187. if (c != '"') {
  7188. throw newSyntaxError('End of string " expected');
  7189. }
  7190. next();
  7191. tokenType = TOKENTYPE.IDENTIFIER;
  7192. return;
  7193. }
  7194. // something unknown is found, wrong characters, a syntax error
  7195. tokenType = TOKENTYPE.UNKNOWN;
  7196. while (c != '') {
  7197. token += c;
  7198. next();
  7199. }
  7200. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7201. }
  7202. /**
  7203. * Parse a graph.
  7204. * @returns {Object} graph
  7205. */
  7206. function parseGraph() {
  7207. var graph = {};
  7208. first();
  7209. getToken();
  7210. // optional strict keyword
  7211. if (token == 'strict') {
  7212. graph.strict = true;
  7213. getToken();
  7214. }
  7215. // graph or digraph keyword
  7216. if (token == 'graph' || token == 'digraph') {
  7217. graph.type = token;
  7218. getToken();
  7219. }
  7220. // optional graph id
  7221. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7222. graph.id = token;
  7223. getToken();
  7224. }
  7225. // open angle bracket
  7226. if (token != '{') {
  7227. throw newSyntaxError('Angle bracket { expected');
  7228. }
  7229. getToken();
  7230. // statements
  7231. parseStatements(graph);
  7232. // close angle bracket
  7233. if (token != '}') {
  7234. throw newSyntaxError('Angle bracket } expected');
  7235. }
  7236. getToken();
  7237. // end of file
  7238. if (token !== '') {
  7239. throw newSyntaxError('End of file expected');
  7240. }
  7241. getToken();
  7242. // remove temporary default properties
  7243. delete graph.node;
  7244. delete graph.edge;
  7245. delete graph.graph;
  7246. return graph;
  7247. }
  7248. /**
  7249. * Parse a list with statements.
  7250. * @param {Object} graph
  7251. */
  7252. function parseStatements (graph) {
  7253. while (token !== '' && token != '}') {
  7254. parseStatement(graph);
  7255. if (token == ';') {
  7256. getToken();
  7257. }
  7258. }
  7259. }
  7260. /**
  7261. * Parse a single statement. Can be a an attribute statement, node
  7262. * statement, a series of node statements and edge statements, or a
  7263. * parameter.
  7264. * @param {Object} graph
  7265. */
  7266. function parseStatement(graph) {
  7267. // parse subgraph
  7268. var subgraph = parseSubgraph(graph);
  7269. if (subgraph) {
  7270. // edge statements
  7271. parseEdge(graph, subgraph);
  7272. return;
  7273. }
  7274. // parse an attribute statement
  7275. var attr = parseAttributeStatement(graph);
  7276. if (attr) {
  7277. return;
  7278. }
  7279. // parse node
  7280. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7281. throw newSyntaxError('Identifier expected');
  7282. }
  7283. var id = token; // id can be a string or a number
  7284. getToken();
  7285. if (token == '=') {
  7286. // id statement
  7287. getToken();
  7288. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7289. throw newSyntaxError('Identifier expected');
  7290. }
  7291. graph[id] = token;
  7292. getToken();
  7293. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7294. }
  7295. else {
  7296. parseNodeStatement(graph, id);
  7297. }
  7298. }
  7299. /**
  7300. * Parse a subgraph
  7301. * @param {Object} graph parent graph object
  7302. * @return {Object | null} subgraph
  7303. */
  7304. function parseSubgraph (graph) {
  7305. var subgraph = null;
  7306. // optional subgraph keyword
  7307. if (token == 'subgraph') {
  7308. subgraph = {};
  7309. subgraph.type = 'subgraph';
  7310. getToken();
  7311. // optional graph id
  7312. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7313. subgraph.id = token;
  7314. getToken();
  7315. }
  7316. }
  7317. // open angle bracket
  7318. if (token == '{') {
  7319. getToken();
  7320. if (!subgraph) {
  7321. subgraph = {};
  7322. }
  7323. subgraph.parent = graph;
  7324. subgraph.node = graph.node;
  7325. subgraph.edge = graph.edge;
  7326. subgraph.graph = graph.graph;
  7327. // statements
  7328. parseStatements(subgraph);
  7329. // close angle bracket
  7330. if (token != '}') {
  7331. throw newSyntaxError('Angle bracket } expected');
  7332. }
  7333. getToken();
  7334. // remove temporary default properties
  7335. delete subgraph.node;
  7336. delete subgraph.edge;
  7337. delete subgraph.graph;
  7338. delete subgraph.parent;
  7339. // register at the parent graph
  7340. if (!graph.subgraphs) {
  7341. graph.subgraphs = [];
  7342. }
  7343. graph.subgraphs.push(subgraph);
  7344. }
  7345. return subgraph;
  7346. }
  7347. /**
  7348. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7349. * Available keywords are 'node', 'edge', 'graph'.
  7350. * The previous list with default attributes will be replaced
  7351. * @param {Object} graph
  7352. * @returns {String | null} keyword Returns the name of the parsed attribute
  7353. * (node, edge, graph), or null if nothing
  7354. * is parsed.
  7355. */
  7356. function parseAttributeStatement (graph) {
  7357. // attribute statements
  7358. if (token == 'node') {
  7359. getToken();
  7360. // node attributes
  7361. graph.node = parseAttributeList();
  7362. return 'node';
  7363. }
  7364. else if (token == 'edge') {
  7365. getToken();
  7366. // edge attributes
  7367. graph.edge = parseAttributeList();
  7368. return 'edge';
  7369. }
  7370. else if (token == 'graph') {
  7371. getToken();
  7372. // graph attributes
  7373. graph.graph = parseAttributeList();
  7374. return 'graph';
  7375. }
  7376. return null;
  7377. }
  7378. /**
  7379. * parse a node statement
  7380. * @param {Object} graph
  7381. * @param {String | Number} id
  7382. */
  7383. function parseNodeStatement(graph, id) {
  7384. // node statement
  7385. var node = {
  7386. id: id
  7387. };
  7388. var attr = parseAttributeList();
  7389. if (attr) {
  7390. node.attr = attr;
  7391. }
  7392. addNode(graph, node);
  7393. // edge statements
  7394. parseEdge(graph, id);
  7395. }
  7396. /**
  7397. * Parse an edge or a series of edges
  7398. * @param {Object} graph
  7399. * @param {String | Number} from Id of the from node
  7400. */
  7401. function parseEdge(graph, from) {
  7402. while (token == '->' || token == '--') {
  7403. var to;
  7404. var type = token;
  7405. getToken();
  7406. var subgraph = parseSubgraph(graph);
  7407. if (subgraph) {
  7408. to = subgraph;
  7409. }
  7410. else {
  7411. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7412. throw newSyntaxError('Identifier or subgraph expected');
  7413. }
  7414. to = token;
  7415. addNode(graph, {
  7416. id: to
  7417. });
  7418. getToken();
  7419. }
  7420. // parse edge attributes
  7421. var attr = parseAttributeList();
  7422. // create edge
  7423. var edge = createEdge(graph, from, to, type, attr);
  7424. addEdge(graph, edge);
  7425. from = to;
  7426. }
  7427. }
  7428. /**
  7429. * Parse a set with attributes,
  7430. * for example [label="1.000", shape=solid]
  7431. * @return {Object | null} attr
  7432. */
  7433. function parseAttributeList() {
  7434. var attr = null;
  7435. while (token == '[') {
  7436. getToken();
  7437. attr = {};
  7438. while (token !== '' && token != ']') {
  7439. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7440. throw newSyntaxError('Attribute name expected');
  7441. }
  7442. var name = token;
  7443. getToken();
  7444. if (token != '=') {
  7445. throw newSyntaxError('Equal sign = expected');
  7446. }
  7447. getToken();
  7448. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7449. throw newSyntaxError('Attribute value expected');
  7450. }
  7451. var value = token;
  7452. setValue(attr, name, value); // name can be a path
  7453. getToken();
  7454. if (token ==',') {
  7455. getToken();
  7456. }
  7457. }
  7458. if (token != ']') {
  7459. throw newSyntaxError('Bracket ] expected');
  7460. }
  7461. getToken();
  7462. }
  7463. return attr;
  7464. }
  7465. /**
  7466. * Create a syntax error with extra information on current token and index.
  7467. * @param {String} message
  7468. * @returns {SyntaxError} err
  7469. */
  7470. function newSyntaxError(message) {
  7471. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7472. }
  7473. /**
  7474. * Chop off text after a maximum length
  7475. * @param {String} text
  7476. * @param {Number} maxLength
  7477. * @returns {String}
  7478. */
  7479. function chop (text, maxLength) {
  7480. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7481. }
  7482. /**
  7483. * Execute a function fn for each pair of elements in two arrays
  7484. * @param {Array | *} array1
  7485. * @param {Array | *} array2
  7486. * @param {function} fn
  7487. */
  7488. function forEach2(array1, array2, fn) {
  7489. if (array1 instanceof Array) {
  7490. array1.forEach(function (elem1) {
  7491. if (array2 instanceof Array) {
  7492. array2.forEach(function (elem2) {
  7493. fn(elem1, elem2);
  7494. });
  7495. }
  7496. else {
  7497. fn(elem1, array2);
  7498. }
  7499. });
  7500. }
  7501. else {
  7502. if (array2 instanceof Array) {
  7503. array2.forEach(function (elem2) {
  7504. fn(array1, elem2);
  7505. });
  7506. }
  7507. else {
  7508. fn(array1, array2);
  7509. }
  7510. }
  7511. }
  7512. /**
  7513. * Convert a string containing a graph in DOT language into a map containing
  7514. * with nodes and edges in the format of graph.
  7515. * @param {String} data Text containing a graph in DOT-notation
  7516. * @return {Object} graphData
  7517. */
  7518. function DOTToGraph (data) {
  7519. // parse the DOT file
  7520. var dotData = parseDOT(data);
  7521. var graphData = {
  7522. nodes: [],
  7523. edges: [],
  7524. options: {}
  7525. };
  7526. // copy the nodes
  7527. if (dotData.nodes) {
  7528. dotData.nodes.forEach(function (dotNode) {
  7529. var graphNode = {
  7530. id: dotNode.id,
  7531. label: String(dotNode.label || dotNode.id)
  7532. };
  7533. merge(graphNode, dotNode.attr);
  7534. if (graphNode.image) {
  7535. graphNode.shape = 'image';
  7536. }
  7537. graphData.nodes.push(graphNode);
  7538. });
  7539. }
  7540. // copy the edges
  7541. if (dotData.edges) {
  7542. /**
  7543. * Convert an edge in DOT format to an edge with VisGraph format
  7544. * @param {Object} dotEdge
  7545. * @returns {Object} graphEdge
  7546. */
  7547. function convertEdge(dotEdge) {
  7548. var graphEdge = {
  7549. from: dotEdge.from,
  7550. to: dotEdge.to
  7551. };
  7552. merge(graphEdge, dotEdge.attr);
  7553. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7554. return graphEdge;
  7555. }
  7556. dotData.edges.forEach(function (dotEdge) {
  7557. var from, to;
  7558. if (dotEdge.from instanceof Object) {
  7559. from = dotEdge.from.nodes;
  7560. }
  7561. else {
  7562. from = {
  7563. id: dotEdge.from
  7564. }
  7565. }
  7566. if (dotEdge.to instanceof Object) {
  7567. to = dotEdge.to.nodes;
  7568. }
  7569. else {
  7570. to = {
  7571. id: dotEdge.to
  7572. }
  7573. }
  7574. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7575. dotEdge.from.edges.forEach(function (subEdge) {
  7576. var graphEdge = convertEdge(subEdge);
  7577. graphData.edges.push(graphEdge);
  7578. });
  7579. }
  7580. forEach2(from, to, function (from, to) {
  7581. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7582. var graphEdge = convertEdge(subEdge);
  7583. graphData.edges.push(graphEdge);
  7584. });
  7585. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7586. dotEdge.to.edges.forEach(function (subEdge) {
  7587. var graphEdge = convertEdge(subEdge);
  7588. graphData.edges.push(graphEdge);
  7589. });
  7590. }
  7591. });
  7592. }
  7593. // copy the options
  7594. if (dotData.attr) {
  7595. graphData.options = dotData.attr;
  7596. }
  7597. return graphData;
  7598. }
  7599. // exports
  7600. exports.parseDOT = parseDOT;
  7601. exports.DOTToGraph = DOTToGraph;
  7602. })(typeof util !== 'undefined' ? util : exports);
  7603. /**
  7604. * Canvas shapes used by the Graph
  7605. */
  7606. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7607. /**
  7608. * Draw a circle shape
  7609. */
  7610. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7611. this.beginPath();
  7612. this.arc(x, y, r, 0, 2*Math.PI, false);
  7613. };
  7614. /**
  7615. * Draw a square shape
  7616. * @param {Number} x horizontal center
  7617. * @param {Number} y vertical center
  7618. * @param {Number} r size, width and height of the square
  7619. */
  7620. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7621. this.beginPath();
  7622. this.rect(x - r, y - r, r * 2, r * 2);
  7623. };
  7624. /**
  7625. * Draw a triangle shape
  7626. * @param {Number} x horizontal center
  7627. * @param {Number} y vertical center
  7628. * @param {Number} r radius, half the length of the sides of the triangle
  7629. */
  7630. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7631. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7632. this.beginPath();
  7633. var s = r * 2;
  7634. var s2 = s / 2;
  7635. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7636. var h = Math.sqrt(s * s - s2 * s2); // height
  7637. this.moveTo(x, y - (h - ir));
  7638. this.lineTo(x + s2, y + ir);
  7639. this.lineTo(x - s2, y + ir);
  7640. this.lineTo(x, y - (h - ir));
  7641. this.closePath();
  7642. };
  7643. /**
  7644. * Draw a triangle shape in downward orientation
  7645. * @param {Number} x horizontal center
  7646. * @param {Number} y vertical center
  7647. * @param {Number} r radius
  7648. */
  7649. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7650. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7651. this.beginPath();
  7652. var s = r * 2;
  7653. var s2 = s / 2;
  7654. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7655. var h = Math.sqrt(s * s - s2 * s2); // height
  7656. this.moveTo(x, y + (h - ir));
  7657. this.lineTo(x + s2, y - ir);
  7658. this.lineTo(x - s2, y - ir);
  7659. this.lineTo(x, y + (h - ir));
  7660. this.closePath();
  7661. };
  7662. /**
  7663. * Draw a star shape, a star with 5 points
  7664. * @param {Number} x horizontal center
  7665. * @param {Number} y vertical center
  7666. * @param {Number} r radius, half the length of the sides of the triangle
  7667. */
  7668. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7669. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7670. this.beginPath();
  7671. for (var n = 0; n < 10; n++) {
  7672. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7673. this.lineTo(
  7674. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7675. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7676. );
  7677. }
  7678. this.closePath();
  7679. };
  7680. /**
  7681. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7682. */
  7683. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7684. var r2d = Math.PI/180;
  7685. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7686. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7687. this.beginPath();
  7688. this.moveTo(x+r,y);
  7689. this.lineTo(x+w-r,y);
  7690. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7691. this.lineTo(x+w,y+h-r);
  7692. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7693. this.lineTo(x+r,y+h);
  7694. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7695. this.lineTo(x,y+r);
  7696. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7697. };
  7698. /**
  7699. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7700. */
  7701. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7702. var kappa = .5522848,
  7703. ox = (w / 2) * kappa, // control point offset horizontal
  7704. oy = (h / 2) * kappa, // control point offset vertical
  7705. xe = x + w, // x-end
  7706. ye = y + h, // y-end
  7707. xm = x + w / 2, // x-middle
  7708. ym = y + h / 2; // y-middle
  7709. this.beginPath();
  7710. this.moveTo(x, ym);
  7711. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7712. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7713. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7714. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7715. };
  7716. /**
  7717. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7718. */
  7719. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7720. var f = 1/3;
  7721. var wEllipse = w;
  7722. var hEllipse = h * f;
  7723. var kappa = .5522848,
  7724. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7725. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7726. xe = x + wEllipse, // x-end
  7727. ye = y + hEllipse, // y-end
  7728. xm = x + wEllipse / 2, // x-middle
  7729. ym = y + hEllipse / 2, // y-middle
  7730. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7731. yeb = y + h; // y-end, bottom ellipse
  7732. this.beginPath();
  7733. this.moveTo(xe, ym);
  7734. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7735. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7736. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7737. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7738. this.lineTo(xe, ymb);
  7739. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7740. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7741. this.lineTo(x, ym);
  7742. };
  7743. /**
  7744. * Draw an arrow point (no line)
  7745. */
  7746. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7747. // tail
  7748. var xt = x - length * Math.cos(angle);
  7749. var yt = y - length * Math.sin(angle);
  7750. // inner tail
  7751. // TODO: allow to customize different shapes
  7752. var xi = x - length * 0.9 * Math.cos(angle);
  7753. var yi = y - length * 0.9 * Math.sin(angle);
  7754. // left
  7755. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7756. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7757. // right
  7758. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7759. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7760. this.beginPath();
  7761. this.moveTo(x, y);
  7762. this.lineTo(xl, yl);
  7763. this.lineTo(xi, yi);
  7764. this.lineTo(xr, yr);
  7765. this.closePath();
  7766. };
  7767. /**
  7768. * Sets up the dashedLine functionality for drawing
  7769. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7770. * @author David Jordan
  7771. * @date 2012-08-08
  7772. */
  7773. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7774. if (!dashArray) dashArray=[10,5];
  7775. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7776. var dashCount = dashArray.length;
  7777. this.moveTo(x, y);
  7778. var dx = (x2-x), dy = (y2-y);
  7779. var slope = dy/dx;
  7780. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7781. var dashIndex=0, draw=true;
  7782. while (distRemaining>=0.1){
  7783. var dashLength = dashArray[dashIndex++%dashCount];
  7784. if (dashLength > distRemaining) dashLength = distRemaining;
  7785. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7786. if (dx<0) xStep = -xStep;
  7787. x += xStep;
  7788. y += slope*xStep;
  7789. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7790. distRemaining -= dashLength;
  7791. draw = !draw;
  7792. }
  7793. };
  7794. // TODO: add diamond shape
  7795. }
  7796. /**
  7797. * @class Node
  7798. * A node. A node can be connected to other nodes via one or multiple edges.
  7799. * @param {object} properties An object containing properties for the node. All
  7800. * properties are optional, except for the id.
  7801. * {number} id Id of the node. Required
  7802. * {string} label Text label for the node
  7803. * {number} x Horizontal position of the node
  7804. * {number} y Vertical position of the node
  7805. * {string} shape Node shape, available:
  7806. * "database", "circle", "ellipse",
  7807. * "box", "image", "text", "dot",
  7808. * "star", "triangle", "triangleDown",
  7809. * "square"
  7810. * {string} image An image url
  7811. * {string} title An title text, can be HTML
  7812. * {anytype} group A group name or number
  7813. * @param {Graph.Images} imagelist A list with images. Only needed
  7814. * when the node has an image
  7815. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7816. * retrieving group properties
  7817. * @param {Object} constants An object with default values for
  7818. * example for the color
  7819. *
  7820. */
  7821. function Node(properties, imagelist, grouplist, constants) {
  7822. this.selected = false;
  7823. this.edges = []; // all edges connected to this node
  7824. this.dynamicEdges = [];
  7825. this.reroutedEdges = {};
  7826. this.group = constants.nodes.group;
  7827. this.fontSize = constants.nodes.fontSize;
  7828. this.fontFace = constants.nodes.fontFace;
  7829. this.fontColor = constants.nodes.fontColor;
  7830. this.fontDrawThreshold = 3;
  7831. this.color = constants.nodes.color;
  7832. // set defaults for the properties
  7833. this.id = undefined;
  7834. this.shape = constants.nodes.shape;
  7835. this.image = constants.nodes.image;
  7836. this.x = null;
  7837. this.y = null;
  7838. this.xFixed = false;
  7839. this.yFixed = false;
  7840. this.horizontalAlignLeft = true; // these are for the navigation controls
  7841. this.verticalAlignTop = true; // these are for the navigation controls
  7842. this.radius = constants.nodes.radius;
  7843. this.baseRadiusValue = constants.nodes.radius;
  7844. this.radiusFixed = false;
  7845. this.radiusMin = constants.nodes.radiusMin;
  7846. this.radiusMax = constants.nodes.radiusMax;
  7847. this.level = -1;
  7848. this.preassignedLevel = false;
  7849. this.imagelist = imagelist;
  7850. this.grouplist = grouplist;
  7851. // physics properties
  7852. this.fx = 0.0; // external force x
  7853. this.fy = 0.0; // external force y
  7854. this.vx = 0.0; // velocity x
  7855. this.vy = 0.0; // velocity y
  7856. this.minForce = constants.minForce;
  7857. this.damping = constants.physics.damping;
  7858. this.mass = 1; // kg
  7859. this.fixedData = {x:null,y:null};
  7860. this.setProperties(properties, constants);
  7861. // creating the variables for clustering
  7862. this.resetCluster();
  7863. this.dynamicEdgesLength = 0;
  7864. this.clusterSession = 0;
  7865. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  7866. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  7867. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  7868. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  7869. this.growthIndicator = 0;
  7870. // variables to tell the node about the graph.
  7871. this.graphScaleInv = 1;
  7872. this.graphScale = 1;
  7873. this.canvasTopLeft = {"x": -300, "y": -300};
  7874. this.canvasBottomRight = {"x": 300, "y": 300};
  7875. this.parentEdgeId = null;
  7876. }
  7877. /**
  7878. * (re)setting the clustering variables and objects
  7879. */
  7880. Node.prototype.resetCluster = function() {
  7881. // clustering variables
  7882. this.formationScale = undefined; // this is used to determine when to open the cluster
  7883. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  7884. this.containedNodes = {};
  7885. this.containedEdges = {};
  7886. this.clusterSessions = [];
  7887. };
  7888. /**
  7889. * Attach a edge to the node
  7890. * @param {Edge} edge
  7891. */
  7892. Node.prototype.attachEdge = function(edge) {
  7893. if (this.edges.indexOf(edge) == -1) {
  7894. this.edges.push(edge);
  7895. }
  7896. if (this.dynamicEdges.indexOf(edge) == -1) {
  7897. this.dynamicEdges.push(edge);
  7898. }
  7899. this.dynamicEdgesLength = this.dynamicEdges.length;
  7900. };
  7901. /**
  7902. * Detach a edge from the node
  7903. * @param {Edge} edge
  7904. */
  7905. Node.prototype.detachEdge = function(edge) {
  7906. var index = this.edges.indexOf(edge);
  7907. if (index != -1) {
  7908. this.edges.splice(index, 1);
  7909. this.dynamicEdges.splice(index, 1);
  7910. }
  7911. this.dynamicEdgesLength = this.dynamicEdges.length;
  7912. };
  7913. /**
  7914. * Set or overwrite properties for the node
  7915. * @param {Object} properties an object with properties
  7916. * @param {Object} constants and object with default, global properties
  7917. */
  7918. Node.prototype.setProperties = function(properties, constants) {
  7919. if (!properties) {
  7920. return;
  7921. }
  7922. this.originalLabel = undefined;
  7923. // basic properties
  7924. if (properties.id !== undefined) {this.id = properties.id;}
  7925. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  7926. if (properties.title !== undefined) {this.title = properties.title;}
  7927. if (properties.group !== undefined) {this.group = properties.group;}
  7928. if (properties.x !== undefined) {this.x = properties.x;}
  7929. if (properties.y !== undefined) {this.y = properties.y;}
  7930. if (properties.value !== undefined) {this.value = properties.value;}
  7931. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  7932. // physics
  7933. if (properties.mass !== undefined) {this.mass = properties.mass;}
  7934. // navigation controls properties
  7935. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  7936. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  7937. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  7938. if (this.id === undefined) {
  7939. throw "Node must have an id";
  7940. }
  7941. // copy group properties
  7942. if (this.group) {
  7943. var groupObj = this.grouplist.get(this.group);
  7944. for (var prop in groupObj) {
  7945. if (groupObj.hasOwnProperty(prop)) {
  7946. this[prop] = groupObj[prop];
  7947. }
  7948. }
  7949. }
  7950. // individual shape properties
  7951. if (properties.shape !== undefined) {this.shape = properties.shape;}
  7952. if (properties.image !== undefined) {this.image = properties.image;}
  7953. if (properties.radius !== undefined) {this.radius = properties.radius;}
  7954. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  7955. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  7956. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  7957. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  7958. if (this.image !== undefined && this.image != "") {
  7959. if (this.imagelist) {
  7960. this.imageObj = this.imagelist.load(this.image);
  7961. }
  7962. else {
  7963. throw "No imagelist provided";
  7964. }
  7965. }
  7966. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  7967. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  7968. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  7969. if (this.shape == 'image') {
  7970. this.radiusMin = constants.nodes.widthMin;
  7971. this.radiusMax = constants.nodes.widthMax;
  7972. }
  7973. // choose draw method depending on the shape
  7974. switch (this.shape) {
  7975. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  7976. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  7977. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  7978. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7979. // TODO: add diamond shape
  7980. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  7981. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  7982. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  7983. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  7984. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  7985. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  7986. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  7987. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7988. }
  7989. // reset the size of the node, this can be changed
  7990. this._reset();
  7991. };
  7992. /**
  7993. * select this node
  7994. */
  7995. Node.prototype.select = function() {
  7996. this.selected = true;
  7997. this._reset();
  7998. };
  7999. /**
  8000. * unselect this node
  8001. */
  8002. Node.prototype.unselect = function() {
  8003. this.selected = false;
  8004. this._reset();
  8005. };
  8006. /**
  8007. * Reset the calculated size of the node, forces it to recalculate its size
  8008. */
  8009. Node.prototype.clearSizeCache = function() {
  8010. this._reset();
  8011. };
  8012. /**
  8013. * Reset the calculated size of the node, forces it to recalculate its size
  8014. * @private
  8015. */
  8016. Node.prototype._reset = function() {
  8017. this.width = undefined;
  8018. this.height = undefined;
  8019. };
  8020. /**
  8021. * get the title of this node.
  8022. * @return {string} title The title of the node, or undefined when no title
  8023. * has been set.
  8024. */
  8025. Node.prototype.getTitle = function() {
  8026. return typeof this.title === "function" ? this.title() : this.title;
  8027. };
  8028. /**
  8029. * Calculate the distance to the border of the Node
  8030. * @param {CanvasRenderingContext2D} ctx
  8031. * @param {Number} angle Angle in radians
  8032. * @returns {number} distance Distance to the border in pixels
  8033. */
  8034. Node.prototype.distanceToBorder = function (ctx, angle) {
  8035. var borderWidth = 1;
  8036. if (!this.width) {
  8037. this.resize(ctx);
  8038. }
  8039. switch (this.shape) {
  8040. case 'circle':
  8041. case 'dot':
  8042. return this.radius + borderWidth;
  8043. case 'ellipse':
  8044. var a = this.width / 2;
  8045. var b = this.height / 2;
  8046. var w = (Math.sin(angle) * a);
  8047. var h = (Math.cos(angle) * b);
  8048. return a * b / Math.sqrt(w * w + h * h);
  8049. // TODO: implement distanceToBorder for database
  8050. // TODO: implement distanceToBorder for triangle
  8051. // TODO: implement distanceToBorder for triangleDown
  8052. case 'box':
  8053. case 'image':
  8054. case 'text':
  8055. default:
  8056. if (this.width) {
  8057. return Math.min(
  8058. Math.abs(this.width / 2 / Math.cos(angle)),
  8059. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  8060. // TODO: reckon with border radius too in case of box
  8061. }
  8062. else {
  8063. return 0;
  8064. }
  8065. }
  8066. // TODO: implement calculation of distance to border for all shapes
  8067. };
  8068. /**
  8069. * Set forces acting on the node
  8070. * @param {number} fx Force in horizontal direction
  8071. * @param {number} fy Force in vertical direction
  8072. */
  8073. Node.prototype._setForce = function(fx, fy) {
  8074. this.fx = fx;
  8075. this.fy = fy;
  8076. };
  8077. /**
  8078. * Add forces acting on the node
  8079. * @param {number} fx Force in horizontal direction
  8080. * @param {number} fy Force in vertical direction
  8081. * @private
  8082. */
  8083. Node.prototype._addForce = function(fx, fy) {
  8084. this.fx += fx;
  8085. this.fy += fy;
  8086. };
  8087. /**
  8088. * Perform one discrete step for the node
  8089. * @param {number} interval Time interval in seconds
  8090. */
  8091. Node.prototype.discreteStep = function(interval) {
  8092. if (!this.xFixed) {
  8093. var dx = this.damping * this.vx; // damping force
  8094. var ax = (this.fx - dx) / this.mass; // acceleration
  8095. this.vx += ax * interval; // velocity
  8096. this.x += this.vx * interval; // position
  8097. }
  8098. if (!this.yFixed) {
  8099. var dy = this.damping * this.vy; // damping force
  8100. var ay = (this.fy - dy) / this.mass; // acceleration
  8101. this.vy += ay * interval; // velocity
  8102. this.y += this.vy * interval; // position
  8103. }
  8104. };
  8105. /**
  8106. * Perform one discrete step for the node
  8107. * @param {number} interval Time interval in seconds
  8108. * @param {number} maxVelocity The speed limit imposed on the velocity
  8109. */
  8110. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  8111. if (!this.xFixed) {
  8112. var dx = this.damping * this.vx; // damping force
  8113. var ax = (this.fx - dx) / this.mass; // acceleration
  8114. this.vx += ax * interval; // velocity
  8115. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  8116. this.x += this.vx * interval; // position
  8117. }
  8118. else {
  8119. this.fx = 0;
  8120. }
  8121. if (!this.yFixed) {
  8122. var dy = this.damping * this.vy; // damping force
  8123. var ay = (this.fy - dy) / this.mass; // acceleration
  8124. this.vy += ay * interval; // velocity
  8125. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  8126. this.y += this.vy * interval; // position
  8127. }
  8128. else {
  8129. this.fy = 0;
  8130. }
  8131. };
  8132. /**
  8133. * Check if this node has a fixed x and y position
  8134. * @return {boolean} true if fixed, false if not
  8135. */
  8136. Node.prototype.isFixed = function() {
  8137. return (this.xFixed && this.yFixed);
  8138. };
  8139. /**
  8140. * Check if this node is moving
  8141. * @param {number} vmin the minimum velocity considered as "moving"
  8142. * @return {boolean} true if moving, false if it has no velocity
  8143. */
  8144. // TODO: replace this method with calculating the kinetic energy
  8145. Node.prototype.isMoving = function(vmin) {
  8146. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  8147. };
  8148. /**
  8149. * check if this node is selecte
  8150. * @return {boolean} selected True if node is selected, else false
  8151. */
  8152. Node.prototype.isSelected = function() {
  8153. return this.selected;
  8154. };
  8155. /**
  8156. * Retrieve the value of the node. Can be undefined
  8157. * @return {Number} value
  8158. */
  8159. Node.prototype.getValue = function() {
  8160. return this.value;
  8161. };
  8162. /**
  8163. * Calculate the distance from the nodes location to the given location (x,y)
  8164. * @param {Number} x
  8165. * @param {Number} y
  8166. * @return {Number} value
  8167. */
  8168. Node.prototype.getDistance = function(x, y) {
  8169. var dx = this.x - x,
  8170. dy = this.y - y;
  8171. return Math.sqrt(dx * dx + dy * dy);
  8172. };
  8173. /**
  8174. * Adjust the value range of the node. The node will adjust it's radius
  8175. * based on its value.
  8176. * @param {Number} min
  8177. * @param {Number} max
  8178. */
  8179. Node.prototype.setValueRange = function(min, max) {
  8180. if (!this.radiusFixed && this.value !== undefined) {
  8181. if (max == min) {
  8182. this.radius = (this.radiusMin + this.radiusMax) / 2;
  8183. }
  8184. else {
  8185. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8186. this.radius = (this.value - min) * scale + this.radiusMin;
  8187. }
  8188. }
  8189. this.baseRadiusValue = this.radius;
  8190. };
  8191. /**
  8192. * Draw this node in the given canvas
  8193. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8194. * @param {CanvasRenderingContext2D} ctx
  8195. */
  8196. Node.prototype.draw = function(ctx) {
  8197. throw "Draw method not initialized for node";
  8198. };
  8199. /**
  8200. * Recalculate the size of this node in the given canvas
  8201. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8202. * @param {CanvasRenderingContext2D} ctx
  8203. */
  8204. Node.prototype.resize = function(ctx) {
  8205. throw "Resize method not initialized for node";
  8206. };
  8207. /**
  8208. * Check if this object is overlapping with the provided object
  8209. * @param {Object} obj an object with parameters left, top, right, bottom
  8210. * @return {boolean} True if location is located on node
  8211. */
  8212. Node.prototype.isOverlappingWith = function(obj) {
  8213. return (this.left < obj.right &&
  8214. this.left + this.width > obj.left &&
  8215. this.top < obj.bottom &&
  8216. this.top + this.height > obj.top);
  8217. };
  8218. Node.prototype._resizeImage = function (ctx) {
  8219. // TODO: pre calculate the image size
  8220. if (!this.width || !this.height) { // undefined or 0
  8221. var width, height;
  8222. if (this.value) {
  8223. this.radius = this.baseRadiusValue;
  8224. var scale = this.imageObj.height / this.imageObj.width;
  8225. if (scale !== undefined) {
  8226. width = this.radius || this.imageObj.width;
  8227. height = this.radius * scale || this.imageObj.height;
  8228. }
  8229. else {
  8230. width = 0;
  8231. height = 0;
  8232. }
  8233. }
  8234. else {
  8235. width = this.imageObj.width;
  8236. height = this.imageObj.height;
  8237. }
  8238. this.width = width;
  8239. this.height = height;
  8240. this.growthIndicator = 0;
  8241. if (this.width > 0 && this.height > 0) {
  8242. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8243. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8244. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8245. this.growthIndicator = this.width - width;
  8246. }
  8247. }
  8248. };
  8249. Node.prototype._drawImage = function (ctx) {
  8250. this._resizeImage(ctx);
  8251. this.left = this.x - this.width / 2;
  8252. this.top = this.y - this.height / 2;
  8253. var yLabel;
  8254. if (this.imageObj.width != 0 ) {
  8255. // draw the shade
  8256. if (this.clusterSize > 1) {
  8257. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  8258. lineWidth *= this.graphScaleInv;
  8259. lineWidth = Math.min(0.2 * this.width,lineWidth);
  8260. ctx.globalAlpha = 0.5;
  8261. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  8262. }
  8263. // draw the image
  8264. ctx.globalAlpha = 1.0;
  8265. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8266. yLabel = this.y + this.height / 2;
  8267. }
  8268. else {
  8269. // image still loading... just draw the label for now
  8270. yLabel = this.y;
  8271. }
  8272. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8273. };
  8274. Node.prototype._resizeBox = function (ctx) {
  8275. if (!this.width) {
  8276. var margin = 5;
  8277. var textSize = this.getTextSize(ctx);
  8278. this.width = textSize.width + 2 * margin;
  8279. this.height = textSize.height + 2 * margin;
  8280. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8281. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8282. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8283. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8284. }
  8285. };
  8286. Node.prototype._drawBox = function (ctx) {
  8287. this._resizeBox(ctx);
  8288. this.left = this.x - this.width / 2;
  8289. this.top = this.y - this.height / 2;
  8290. var clusterLineWidth = 2.5;
  8291. var selectionLineWidth = 2;
  8292. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8293. // draw the outer border
  8294. if (this.clusterSize > 1) {
  8295. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8296. ctx.lineWidth *= this.graphScaleInv;
  8297. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8298. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  8299. ctx.stroke();
  8300. }
  8301. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8302. ctx.lineWidth *= this.graphScaleInv;
  8303. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8304. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8305. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8306. ctx.fill();
  8307. ctx.stroke();
  8308. this._label(ctx, this.label, this.x, this.y);
  8309. };
  8310. Node.prototype._resizeDatabase = function (ctx) {
  8311. if (!this.width) {
  8312. var margin = 5;
  8313. var textSize = this.getTextSize(ctx);
  8314. var size = textSize.width + 2 * margin;
  8315. this.width = size;
  8316. this.height = size;
  8317. // scaling used for clustering
  8318. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8319. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8320. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8321. this.growthIndicator = this.width - size;
  8322. }
  8323. };
  8324. Node.prototype._drawDatabase = function (ctx) {
  8325. this._resizeDatabase(ctx);
  8326. this.left = this.x - this.width / 2;
  8327. this.top = this.y - this.height / 2;
  8328. var clusterLineWidth = 2.5;
  8329. var selectionLineWidth = 2;
  8330. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8331. // draw the outer border
  8332. if (this.clusterSize > 1) {
  8333. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8334. ctx.lineWidth *= this.graphScaleInv;
  8335. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8336. 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);
  8337. ctx.stroke();
  8338. }
  8339. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8340. ctx.lineWidth *= this.graphScaleInv;
  8341. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8342. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8343. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8344. ctx.fill();
  8345. ctx.stroke();
  8346. this._label(ctx, this.label, this.x, this.y);
  8347. };
  8348. Node.prototype._resizeCircle = function (ctx) {
  8349. if (!this.width) {
  8350. var margin = 5;
  8351. var textSize = this.getTextSize(ctx);
  8352. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8353. this.radius = diameter / 2;
  8354. this.width = diameter;
  8355. this.height = diameter;
  8356. // scaling used for clustering
  8357. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8358. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8359. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8360. this.growthIndicator = this.radius - 0.5*diameter;
  8361. }
  8362. };
  8363. Node.prototype._drawCircle = function (ctx) {
  8364. this._resizeCircle(ctx);
  8365. this.left = this.x - this.width / 2;
  8366. this.top = this.y - this.height / 2;
  8367. var clusterLineWidth = 2.5;
  8368. var selectionLineWidth = 2;
  8369. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8370. // draw the outer border
  8371. if (this.clusterSize > 1) {
  8372. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8373. ctx.lineWidth *= this.graphScaleInv;
  8374. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8375. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  8376. ctx.stroke();
  8377. }
  8378. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8379. ctx.lineWidth *= this.graphScaleInv;
  8380. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8381. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8382. ctx.circle(this.x, this.y, this.radius);
  8383. ctx.fill();
  8384. ctx.stroke();
  8385. this._label(ctx, this.label, this.x, this.y);
  8386. };
  8387. Node.prototype._resizeEllipse = function (ctx) {
  8388. if (!this.width) {
  8389. var textSize = this.getTextSize(ctx);
  8390. this.width = textSize.width * 1.5;
  8391. this.height = textSize.height * 2;
  8392. if (this.width < this.height) {
  8393. this.width = this.height;
  8394. }
  8395. var defaultSize = this.width;
  8396. // scaling used for clustering
  8397. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8398. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8399. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8400. this.growthIndicator = this.width - defaultSize;
  8401. }
  8402. };
  8403. Node.prototype._drawEllipse = function (ctx) {
  8404. this._resizeEllipse(ctx);
  8405. this.left = this.x - this.width / 2;
  8406. this.top = this.y - this.height / 2;
  8407. var clusterLineWidth = 2.5;
  8408. var selectionLineWidth = 2;
  8409. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8410. // draw the outer border
  8411. if (this.clusterSize > 1) {
  8412. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8413. ctx.lineWidth *= this.graphScaleInv;
  8414. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8415. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  8416. ctx.stroke();
  8417. }
  8418. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8419. ctx.lineWidth *= this.graphScaleInv;
  8420. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8421. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8422. ctx.ellipse(this.left, this.top, this.width, this.height);
  8423. ctx.fill();
  8424. ctx.stroke();
  8425. this._label(ctx, this.label, this.x, this.y);
  8426. };
  8427. Node.prototype._drawDot = function (ctx) {
  8428. this._drawShape(ctx, 'circle');
  8429. };
  8430. Node.prototype._drawTriangle = function (ctx) {
  8431. this._drawShape(ctx, 'triangle');
  8432. };
  8433. Node.prototype._drawTriangleDown = function (ctx) {
  8434. this._drawShape(ctx, 'triangleDown');
  8435. };
  8436. Node.prototype._drawSquare = function (ctx) {
  8437. this._drawShape(ctx, 'square');
  8438. };
  8439. Node.prototype._drawStar = function (ctx) {
  8440. this._drawShape(ctx, 'star');
  8441. };
  8442. Node.prototype._resizeShape = function (ctx) {
  8443. if (!this.width) {
  8444. this.radius = this.baseRadiusValue;
  8445. var size = 2 * this.radius;
  8446. this.width = size;
  8447. this.height = size;
  8448. // scaling used for clustering
  8449. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8450. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8451. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8452. this.growthIndicator = this.width - size;
  8453. }
  8454. };
  8455. Node.prototype._drawShape = function (ctx, shape) {
  8456. this._resizeShape(ctx);
  8457. this.left = this.x - this.width / 2;
  8458. this.top = this.y - this.height / 2;
  8459. var clusterLineWidth = 2.5;
  8460. var selectionLineWidth = 2;
  8461. var radiusMultiplier = 2;
  8462. // choose draw method depending on the shape
  8463. switch (shape) {
  8464. case 'dot': radiusMultiplier = 2; break;
  8465. case 'square': radiusMultiplier = 2; break;
  8466. case 'triangle': radiusMultiplier = 3; break;
  8467. case 'triangleDown': radiusMultiplier = 3; break;
  8468. case 'star': radiusMultiplier = 4; break;
  8469. }
  8470. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8471. // draw the outer border
  8472. if (this.clusterSize > 1) {
  8473. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8474. ctx.lineWidth *= this.graphScaleInv;
  8475. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8476. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  8477. ctx.stroke();
  8478. }
  8479. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8480. ctx.lineWidth *= this.graphScaleInv;
  8481. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8482. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8483. ctx[shape](this.x, this.y, this.radius);
  8484. ctx.fill();
  8485. ctx.stroke();
  8486. if (this.label) {
  8487. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8488. }
  8489. };
  8490. Node.prototype._resizeText = function (ctx) {
  8491. if (!this.width) {
  8492. var margin = 5;
  8493. var textSize = this.getTextSize(ctx);
  8494. this.width = textSize.width + 2 * margin;
  8495. this.height = textSize.height + 2 * margin;
  8496. // scaling used for clustering
  8497. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8498. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8499. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8500. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8501. }
  8502. };
  8503. Node.prototype._drawText = function (ctx) {
  8504. this._resizeText(ctx);
  8505. this.left = this.x - this.width / 2;
  8506. this.top = this.y - this.height / 2;
  8507. this._label(ctx, this.label, this.x, this.y);
  8508. };
  8509. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8510. if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
  8511. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8512. ctx.fillStyle = this.fontColor || "black";
  8513. ctx.textAlign = align || "center";
  8514. ctx.textBaseline = baseline || "middle";
  8515. var lines = text.split('\n'),
  8516. lineCount = lines.length,
  8517. fontSize = (this.fontSize + 4),
  8518. yLine = y + (1 - lineCount) / 2 * fontSize;
  8519. for (var i = 0; i < lineCount; i++) {
  8520. ctx.fillText(lines[i], x, yLine);
  8521. yLine += fontSize;
  8522. }
  8523. }
  8524. };
  8525. Node.prototype.getTextSize = function(ctx) {
  8526. if (this.label !== undefined) {
  8527. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8528. var lines = this.label.split('\n'),
  8529. height = (this.fontSize + 4) * lines.length,
  8530. width = 0;
  8531. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8532. width = Math.max(width, ctx.measureText(lines[i]).width);
  8533. }
  8534. return {"width": width, "height": height};
  8535. }
  8536. else {
  8537. return {"width": 0, "height": 0};
  8538. }
  8539. };
  8540. /**
  8541. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  8542. * there is a safety margin of 0.3 * width;
  8543. *
  8544. * @returns {boolean}
  8545. */
  8546. Node.prototype.inArea = function() {
  8547. if (this.width !== undefined) {
  8548. return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
  8549. this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
  8550. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  8551. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  8552. }
  8553. else {
  8554. return true;
  8555. }
  8556. };
  8557. /**
  8558. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  8559. * @returns {boolean}
  8560. */
  8561. Node.prototype.inView = function() {
  8562. return (this.x >= this.canvasTopLeft.x &&
  8563. this.x < this.canvasBottomRight.x &&
  8564. this.y >= this.canvasTopLeft.y &&
  8565. this.y < this.canvasBottomRight.y);
  8566. };
  8567. /**
  8568. * This allows the zoom level of the graph to influence the rendering
  8569. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  8570. *
  8571. * @param scale
  8572. * @param canvasTopLeft
  8573. * @param canvasBottomRight
  8574. */
  8575. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  8576. this.graphScaleInv = 1.0/scale;
  8577. this.graphScale = scale;
  8578. this.canvasTopLeft = canvasTopLeft;
  8579. this.canvasBottomRight = canvasBottomRight;
  8580. };
  8581. /**
  8582. * This allows the zoom level of the graph to influence the rendering
  8583. *
  8584. * @param scale
  8585. */
  8586. Node.prototype.setScale = function(scale) {
  8587. this.graphScaleInv = 1.0/scale;
  8588. this.graphScale = scale;
  8589. };
  8590. /**
  8591. * set the velocity at 0. Is called when this node is contained in another during clustering
  8592. */
  8593. Node.prototype.clearVelocity = function() {
  8594. this.vx = 0;
  8595. this.vy = 0;
  8596. };
  8597. /**
  8598. * Basic preservation of (kinectic) energy
  8599. *
  8600. * @param massBeforeClustering
  8601. */
  8602. Node.prototype.updateVelocity = function(massBeforeClustering) {
  8603. var energyBefore = this.vx * this.vx * massBeforeClustering;
  8604. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8605. this.vx = Math.sqrt(energyBefore/this.mass);
  8606. energyBefore = this.vy * this.vy * massBeforeClustering;
  8607. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8608. this.vy = Math.sqrt(energyBefore/this.mass);
  8609. };
  8610. /**
  8611. * @class Edge
  8612. *
  8613. * A edge connects two nodes
  8614. * @param {Object} properties Object with properties. Must contain
  8615. * At least properties from and to.
  8616. * Available properties: from (number),
  8617. * to (number), label (string, color (string),
  8618. * width (number), style (string),
  8619. * length (number), title (string)
  8620. * @param {Graph} graph A graph object, used to find and edge to
  8621. * nodes.
  8622. * @param {Object} constants An object with default values for
  8623. * example for the color
  8624. */
  8625. function Edge (properties, graph, constants) {
  8626. if (!graph) {
  8627. throw "No graph provided";
  8628. }
  8629. this.graph = graph;
  8630. // initialize constants
  8631. this.widthMin = constants.edges.widthMin;
  8632. this.widthMax = constants.edges.widthMax;
  8633. // initialize variables
  8634. this.id = undefined;
  8635. this.fromId = undefined;
  8636. this.toId = undefined;
  8637. this.style = constants.edges.style;
  8638. this.title = undefined;
  8639. this.width = constants.edges.width;
  8640. this.value = undefined;
  8641. this.length = constants.physics.springLength;
  8642. this.customLength = false;
  8643. this.selected = false;
  8644. this.smooth = constants.smoothCurves;
  8645. this.arrowScaleFactor = constants.edges.arrowScaleFactor;
  8646. this.from = null; // a node
  8647. this.to = null; // a node
  8648. this.via = null; // a temp node
  8649. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  8650. // by storing the original information we can revert to the original connection when the cluser is opened.
  8651. this.originalFromId = [];
  8652. this.originalToId = [];
  8653. this.connected = false;
  8654. // Added to support dashed lines
  8655. // David Jordan
  8656. // 2012-08-08
  8657. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8658. this.color = {color:constants.edges.color.color,
  8659. highlight:constants.edges.color.highlight};
  8660. this.widthFixed = false;
  8661. this.lengthFixed = false;
  8662. this.setProperties(properties, constants);
  8663. }
  8664. /**
  8665. * Set or overwrite properties for the edge
  8666. * @param {Object} properties an object with properties
  8667. * @param {Object} constants and object with default, global properties
  8668. */
  8669. Edge.prototype.setProperties = function(properties, constants) {
  8670. if (!properties) {
  8671. return;
  8672. }
  8673. if (properties.from !== undefined) {this.fromId = properties.from;}
  8674. if (properties.to !== undefined) {this.toId = properties.to;}
  8675. if (properties.id !== undefined) {this.id = properties.id;}
  8676. if (properties.style !== undefined) {this.style = properties.style;}
  8677. if (properties.label !== undefined) {this.label = properties.label;}
  8678. if (this.label) {
  8679. this.fontSize = constants.edges.fontSize;
  8680. this.fontFace = constants.edges.fontFace;
  8681. this.fontColor = constants.edges.fontColor;
  8682. this.fontFill = constants.edges.fontFill;
  8683. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8684. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8685. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8686. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  8687. }
  8688. if (properties.title !== undefined) {this.title = properties.title;}
  8689. if (properties.width !== undefined) {this.width = properties.width;}
  8690. if (properties.value !== undefined) {this.value = properties.value;}
  8691. if (properties.length !== undefined) {this.length = properties.length;
  8692. this.customLength = true;}
  8693. // scale the arrow
  8694. if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
  8695. // Added to support dashed lines
  8696. // David Jordan
  8697. // 2012-08-08
  8698. if (properties.dash) {
  8699. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  8700. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  8701. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  8702. }
  8703. if (properties.color !== undefined) {
  8704. if (util.isString(properties.color)) {
  8705. this.color.color = properties.color;
  8706. this.color.highlight = properties.color;
  8707. }
  8708. else {
  8709. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  8710. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  8711. }
  8712. }
  8713. // A node is connected when it has a from and to node.
  8714. this.connect();
  8715. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  8716. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  8717. // set draw method based on style
  8718. switch (this.style) {
  8719. case 'line': this.draw = this._drawLine; break;
  8720. case 'arrow': this.draw = this._drawArrow; break;
  8721. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8722. case 'dash-line': this.draw = this._drawDashLine; break;
  8723. default: this.draw = this._drawLine; break;
  8724. }
  8725. };
  8726. /**
  8727. * Connect an edge to its nodes
  8728. */
  8729. Edge.prototype.connect = function () {
  8730. this.disconnect();
  8731. this.from = this.graph.nodes[this.fromId] || null;
  8732. this.to = this.graph.nodes[this.toId] || null;
  8733. this.connected = (this.from && this.to);
  8734. if (this.connected) {
  8735. this.from.attachEdge(this);
  8736. this.to.attachEdge(this);
  8737. }
  8738. else {
  8739. if (this.from) {
  8740. this.from.detachEdge(this);
  8741. }
  8742. if (this.to) {
  8743. this.to.detachEdge(this);
  8744. }
  8745. }
  8746. };
  8747. /**
  8748. * Disconnect an edge from its nodes
  8749. */
  8750. Edge.prototype.disconnect = function () {
  8751. if (this.from) {
  8752. this.from.detachEdge(this);
  8753. this.from = null;
  8754. }
  8755. if (this.to) {
  8756. this.to.detachEdge(this);
  8757. this.to = null;
  8758. }
  8759. this.connected = false;
  8760. };
  8761. /**
  8762. * get the title of this edge.
  8763. * @return {string} title The title of the edge, or undefined when no title
  8764. * has been set.
  8765. */
  8766. Edge.prototype.getTitle = function() {
  8767. return typeof this.title === "function" ? this.title() : this.title;
  8768. };
  8769. /**
  8770. * Retrieve the value of the edge. Can be undefined
  8771. * @return {Number} value
  8772. */
  8773. Edge.prototype.getValue = function() {
  8774. return this.value;
  8775. };
  8776. /**
  8777. * Adjust the value range of the edge. The edge will adjust it's width
  8778. * based on its value.
  8779. * @param {Number} min
  8780. * @param {Number} max
  8781. */
  8782. Edge.prototype.setValueRange = function(min, max) {
  8783. if (!this.widthFixed && this.value !== undefined) {
  8784. var scale = (this.widthMax - this.widthMin) / (max - min);
  8785. this.width = (this.value - min) * scale + this.widthMin;
  8786. }
  8787. };
  8788. /**
  8789. * Redraw a edge
  8790. * Draw this edge in the given canvas
  8791. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8792. * @param {CanvasRenderingContext2D} ctx
  8793. */
  8794. Edge.prototype.draw = function(ctx) {
  8795. throw "Method draw not initialized in edge";
  8796. };
  8797. /**
  8798. * Check if this object is overlapping with the provided object
  8799. * @param {Object} obj an object with parameters left, top
  8800. * @return {boolean} True if location is located on the edge
  8801. */
  8802. Edge.prototype.isOverlappingWith = function(obj) {
  8803. if (this.connected) {
  8804. var distMax = 10;
  8805. var xFrom = this.from.x;
  8806. var yFrom = this.from.y;
  8807. var xTo = this.to.x;
  8808. var yTo = this.to.y;
  8809. var xObj = obj.left;
  8810. var yObj = obj.top;
  8811. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8812. return (dist < distMax);
  8813. }
  8814. else {
  8815. return false
  8816. }
  8817. };
  8818. /**
  8819. * Redraw a edge as a line
  8820. * Draw this edge in the given canvas
  8821. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8822. * @param {CanvasRenderingContext2D} ctx
  8823. * @private
  8824. */
  8825. Edge.prototype._drawLine = function(ctx) {
  8826. // set style
  8827. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  8828. else {ctx.strokeStyle = this.color.color;}
  8829. ctx.lineWidth = this._getLineWidth();
  8830. if (this.from != this.to) {
  8831. // draw line
  8832. this._line(ctx);
  8833. // draw label
  8834. var point;
  8835. if (this.label) {
  8836. if (this.smooth == true) {
  8837. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  8838. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  8839. point = {x:midpointX, y:midpointY};
  8840. }
  8841. else {
  8842. point = this._pointOnLine(0.5);
  8843. }
  8844. this._label(ctx, this.label, point.x, point.y);
  8845. }
  8846. }
  8847. else {
  8848. var x, y;
  8849. var radius = this.length / 4;
  8850. var node = this.from;
  8851. if (!node.width) {
  8852. node.resize(ctx);
  8853. }
  8854. if (node.width > node.height) {
  8855. x = node.x + node.width / 2;
  8856. y = node.y - radius;
  8857. }
  8858. else {
  8859. x = node.x + radius;
  8860. y = node.y - node.height / 2;
  8861. }
  8862. this._circle(ctx, x, y, radius);
  8863. point = this._pointOnCircle(x, y, radius, 0.5);
  8864. this._label(ctx, this.label, point.x, point.y);
  8865. }
  8866. };
  8867. /**
  8868. * Get the line width of the edge. Depends on width and whether one of the
  8869. * connected nodes is selected.
  8870. * @return {Number} width
  8871. * @private
  8872. */
  8873. Edge.prototype._getLineWidth = function() {
  8874. if (this.selected == true) {
  8875. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  8876. }
  8877. else {
  8878. return this.width*this.graphScaleInv;
  8879. }
  8880. };
  8881. /**
  8882. * Draw a line between two nodes
  8883. * @param {CanvasRenderingContext2D} ctx
  8884. * @private
  8885. */
  8886. Edge.prototype._line = function (ctx) {
  8887. // draw a straight line
  8888. ctx.beginPath();
  8889. ctx.moveTo(this.from.x, this.from.y);
  8890. if (this.smooth == true) {
  8891. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  8892. }
  8893. else {
  8894. ctx.lineTo(this.to.x, this.to.y);
  8895. }
  8896. ctx.stroke();
  8897. };
  8898. /**
  8899. * Draw a line from a node to itself, a circle
  8900. * @param {CanvasRenderingContext2D} ctx
  8901. * @param {Number} x
  8902. * @param {Number} y
  8903. * @param {Number} radius
  8904. * @private
  8905. */
  8906. Edge.prototype._circle = function (ctx, x, y, radius) {
  8907. // draw a circle
  8908. ctx.beginPath();
  8909. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8910. ctx.stroke();
  8911. };
  8912. /**
  8913. * Draw label with white background and with the middle at (x, y)
  8914. * @param {CanvasRenderingContext2D} ctx
  8915. * @param {String} text
  8916. * @param {Number} x
  8917. * @param {Number} y
  8918. * @private
  8919. */
  8920. Edge.prototype._label = function (ctx, text, x, y) {
  8921. if (text) {
  8922. // TODO: cache the calculated size
  8923. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8924. this.fontSize + "px " + this.fontFace;
  8925. ctx.fillStyle = this.fontFill;
  8926. var width = ctx.measureText(text).width;
  8927. var height = this.fontSize;
  8928. var left = x - width / 2;
  8929. var top = y - height / 2;
  8930. ctx.fillRect(left, top, width, height);
  8931. // draw text
  8932. ctx.fillStyle = this.fontColor || "black";
  8933. ctx.textAlign = "left";
  8934. ctx.textBaseline = "top";
  8935. ctx.fillText(text, left, top);
  8936. }
  8937. };
  8938. /**
  8939. * Redraw a edge as a dashed line
  8940. * Draw this edge in the given canvas
  8941. * @author David Jordan
  8942. * @date 2012-08-08
  8943. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8944. * @param {CanvasRenderingContext2D} ctx
  8945. * @private
  8946. */
  8947. Edge.prototype._drawDashLine = function(ctx) {
  8948. // set style
  8949. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  8950. else {ctx.strokeStyle = this.color.color;}
  8951. ctx.lineWidth = this._getLineWidth();
  8952. // only firefox and chrome support this method, else we use the legacy one.
  8953. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  8954. ctx.beginPath();
  8955. ctx.moveTo(this.from.x, this.from.y);
  8956. // configure the dash pattern
  8957. var pattern = [0];
  8958. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  8959. pattern = [this.dash.length,this.dash.gap];
  8960. }
  8961. else {
  8962. pattern = [5,5];
  8963. }
  8964. // set dash settings for chrome or firefox
  8965. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  8966. ctx.setLineDash(pattern);
  8967. ctx.lineDashOffset = 0;
  8968. } else { //Firefox
  8969. ctx.mozDash = pattern;
  8970. ctx.mozDashOffset = 0;
  8971. }
  8972. // draw the line
  8973. if (this.smooth == true) {
  8974. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  8975. }
  8976. else {
  8977. ctx.lineTo(this.to.x, this.to.y);
  8978. }
  8979. ctx.stroke();
  8980. // restore the dash settings.
  8981. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  8982. ctx.setLineDash([0]);
  8983. ctx.lineDashOffset = 0;
  8984. } else { //Firefox
  8985. ctx.mozDash = [0];
  8986. ctx.mozDashOffset = 0;
  8987. }
  8988. }
  8989. else { // unsupporting smooth lines
  8990. // draw dashed line
  8991. ctx.beginPath();
  8992. ctx.lineCap = 'round';
  8993. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  8994. {
  8995. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8996. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  8997. }
  8998. 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
  8999. {
  9000. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9001. [this.dash.length,this.dash.gap]);
  9002. }
  9003. else //If all else fails draw a line
  9004. {
  9005. ctx.moveTo(this.from.x, this.from.y);
  9006. ctx.lineTo(this.to.x, this.to.y);
  9007. }
  9008. ctx.stroke();
  9009. }
  9010. // draw label
  9011. if (this.label) {
  9012. var point;
  9013. if (this.smooth == true) {
  9014. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9015. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9016. point = {x:midpointX, y:midpointY};
  9017. }
  9018. else {
  9019. point = this._pointOnLine(0.5);
  9020. }
  9021. this._label(ctx, this.label, point.x, point.y);
  9022. }
  9023. };
  9024. /**
  9025. * Get a point on a line
  9026. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9027. * @return {Object} point
  9028. * @private
  9029. */
  9030. Edge.prototype._pointOnLine = function (percentage) {
  9031. return {
  9032. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  9033. y: (1 - percentage) * this.from.y + percentage * this.to.y
  9034. }
  9035. };
  9036. /**
  9037. * Get a point on a circle
  9038. * @param {Number} x
  9039. * @param {Number} y
  9040. * @param {Number} radius
  9041. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9042. * @return {Object} point
  9043. * @private
  9044. */
  9045. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  9046. var angle = (percentage - 3/8) * 2 * Math.PI;
  9047. return {
  9048. x: x + radius * Math.cos(angle),
  9049. y: y - radius * Math.sin(angle)
  9050. }
  9051. };
  9052. /**
  9053. * Redraw a edge as a line with an arrow halfway the line
  9054. * Draw this edge in the given canvas
  9055. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9056. * @param {CanvasRenderingContext2D} ctx
  9057. * @private
  9058. */
  9059. Edge.prototype._drawArrowCenter = function(ctx) {
  9060. var point;
  9061. // set style
  9062. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9063. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9064. ctx.lineWidth = this._getLineWidth();
  9065. if (this.from != this.to) {
  9066. // draw line
  9067. this._line(ctx);
  9068. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9069. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9070. // draw an arrow halfway the line
  9071. if (this.smooth == true) {
  9072. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9073. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9074. point = {x:midpointX, y:midpointY};
  9075. }
  9076. else {
  9077. point = this._pointOnLine(0.5);
  9078. }
  9079. ctx.arrow(point.x, point.y, angle, length);
  9080. ctx.fill();
  9081. ctx.stroke();
  9082. // draw label
  9083. if (this.label) {
  9084. this._label(ctx, this.label, point.x, point.y);
  9085. }
  9086. }
  9087. else {
  9088. // draw circle
  9089. var x, y;
  9090. var radius = 0.25 * Math.max(100,this.length);
  9091. var node = this.from;
  9092. if (!node.width) {
  9093. node.resize(ctx);
  9094. }
  9095. if (node.width > node.height) {
  9096. x = node.x + node.width * 0.5;
  9097. y = node.y - radius;
  9098. }
  9099. else {
  9100. x = node.x + radius;
  9101. y = node.y - node.height * 0.5;
  9102. }
  9103. this._circle(ctx, x, y, radius);
  9104. // draw all arrows
  9105. var angle = 0.2 * Math.PI;
  9106. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9107. point = this._pointOnCircle(x, y, radius, 0.5);
  9108. ctx.arrow(point.x, point.y, angle, length);
  9109. ctx.fill();
  9110. ctx.stroke();
  9111. // draw label
  9112. if (this.label) {
  9113. point = this._pointOnCircle(x, y, radius, 0.5);
  9114. this._label(ctx, this.label, point.x, point.y);
  9115. }
  9116. }
  9117. };
  9118. /**
  9119. * Redraw a edge as a line with an arrow
  9120. * Draw this edge in the given canvas
  9121. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9122. * @param {CanvasRenderingContext2D} ctx
  9123. * @private
  9124. */
  9125. Edge.prototype._drawArrow = function(ctx) {
  9126. // set style
  9127. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9128. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9129. ctx.lineWidth = this._getLineWidth();
  9130. var angle, length;
  9131. //draw a line
  9132. if (this.from != this.to) {
  9133. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9134. var dx = (this.to.x - this.from.x);
  9135. var dy = (this.to.y - this.from.y);
  9136. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9137. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  9138. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  9139. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  9140. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  9141. if (this.smooth == true) {
  9142. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  9143. dx = (this.to.x - this.via.x);
  9144. dy = (this.to.y - this.via.y);
  9145. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9146. }
  9147. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  9148. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  9149. var xTo,yTo;
  9150. if (this.smooth == true) {
  9151. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  9152. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  9153. }
  9154. else {
  9155. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  9156. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  9157. }
  9158. ctx.beginPath();
  9159. ctx.moveTo(xFrom,yFrom);
  9160. if (this.smooth == true) {
  9161. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  9162. }
  9163. else {
  9164. ctx.lineTo(xTo, yTo);
  9165. }
  9166. ctx.stroke();
  9167. // draw arrow at the end of the line
  9168. length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9169. ctx.arrow(xTo, yTo, angle, length);
  9170. ctx.fill();
  9171. ctx.stroke();
  9172. // draw label
  9173. if (this.label) {
  9174. var point;
  9175. if (this.smooth == true) {
  9176. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9177. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9178. point = {x:midpointX, y:midpointY};
  9179. }
  9180. else {
  9181. point = this._pointOnLine(0.5);
  9182. }
  9183. this._label(ctx, this.label, point.x, point.y);
  9184. }
  9185. }
  9186. else {
  9187. // draw circle
  9188. var node = this.from;
  9189. var x, y, arrow;
  9190. var radius = 0.25 * Math.max(100,this.length);
  9191. if (!node.width) {
  9192. node.resize(ctx);
  9193. }
  9194. if (node.width > node.height) {
  9195. x = node.x + node.width * 0.5;
  9196. y = node.y - radius;
  9197. arrow = {
  9198. x: x,
  9199. y: node.y,
  9200. angle: 0.9 * Math.PI
  9201. };
  9202. }
  9203. else {
  9204. x = node.x + radius;
  9205. y = node.y - node.height * 0.5;
  9206. arrow = {
  9207. x: node.x,
  9208. y: y,
  9209. angle: 0.6 * Math.PI
  9210. };
  9211. }
  9212. ctx.beginPath();
  9213. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9214. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9215. ctx.stroke();
  9216. // draw all arrows
  9217. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9218. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9219. ctx.fill();
  9220. ctx.stroke();
  9221. // draw label
  9222. if (this.label) {
  9223. point = this._pointOnCircle(x, y, radius, 0.5);
  9224. this._label(ctx, this.label, point.x, point.y);
  9225. }
  9226. }
  9227. };
  9228. /**
  9229. * Calculate the distance between a point (x3,y3) and a line segment from
  9230. * (x1,y1) to (x2,y2).
  9231. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9232. * @param {number} x1
  9233. * @param {number} y1
  9234. * @param {number} x2
  9235. * @param {number} y2
  9236. * @param {number} x3
  9237. * @param {number} y3
  9238. * @private
  9239. */
  9240. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9241. if (this.smooth == true) {
  9242. var minDistance = 1e9;
  9243. var i,t,x,y,dx,dy;
  9244. for (i = 0; i < 10; i++) {
  9245. t = 0.1*i;
  9246. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  9247. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  9248. dx = Math.abs(x3-x);
  9249. dy = Math.abs(y3-y);
  9250. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  9251. }
  9252. return minDistance
  9253. }
  9254. else {
  9255. var px = x2-x1,
  9256. py = y2-y1,
  9257. something = px*px + py*py,
  9258. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9259. if (u > 1) {
  9260. u = 1;
  9261. }
  9262. else if (u < 0) {
  9263. u = 0;
  9264. }
  9265. var x = x1 + u * px,
  9266. y = y1 + u * py,
  9267. dx = x - x3,
  9268. dy = y - y3;
  9269. //# Note: If the actual distance does not matter,
  9270. //# if you only want to compare what this function
  9271. //# returns to other results of this function, you
  9272. //# can just return the squared distance instead
  9273. //# (i.e. remove the sqrt) to gain a little performance
  9274. return Math.sqrt(dx*dx + dy*dy);
  9275. }
  9276. };
  9277. /**
  9278. * This allows the zoom level of the graph to influence the rendering
  9279. *
  9280. * @param scale
  9281. */
  9282. Edge.prototype.setScale = function(scale) {
  9283. this.graphScaleInv = 1.0/scale;
  9284. };
  9285. Edge.prototype.select = function() {
  9286. this.selected = true;
  9287. };
  9288. Edge.prototype.unselect = function() {
  9289. this.selected = false;
  9290. };
  9291. Edge.prototype.positionBezierNode = function() {
  9292. if (this.via !== null) {
  9293. this.via.x = 0.5 * (this.from.x + this.to.x);
  9294. this.via.y = 0.5 * (this.from.y + this.to.y);
  9295. }
  9296. };
  9297. /**
  9298. * Popup is a class to create a popup window with some text
  9299. * @param {Element} container The container object.
  9300. * @param {Number} [x]
  9301. * @param {Number} [y]
  9302. * @param {String} [text]
  9303. * @param {Object} [style] An object containing borderColor,
  9304. * backgroundColor, etc.
  9305. */
  9306. function Popup(container, x, y, text, style) {
  9307. if (container) {
  9308. this.container = container;
  9309. }
  9310. else {
  9311. this.container = document.body;
  9312. }
  9313. // x, y and text are optional, see if a style object was passed in their place
  9314. if (style === undefined) {
  9315. if (typeof x === "object") {
  9316. style = x;
  9317. x = undefined;
  9318. } else if (typeof text === "object") {
  9319. style = text;
  9320. text = undefined;
  9321. } else {
  9322. // for backwards compatibility, in case clients other than Graph are creating Popup directly
  9323. style = {
  9324. fontColor: 'black',
  9325. fontSize: 14, // px
  9326. fontFace: 'verdana',
  9327. color: {
  9328. border: '#666',
  9329. background: '#FFFFC6'
  9330. }
  9331. }
  9332. }
  9333. }
  9334. this.x = 0;
  9335. this.y = 0;
  9336. this.padding = 5;
  9337. if (x !== undefined && y !== undefined ) {
  9338. this.setPosition(x, y);
  9339. }
  9340. if (text !== undefined) {
  9341. this.setText(text);
  9342. }
  9343. // create the frame
  9344. this.frame = document.createElement("div");
  9345. var styleAttr = this.frame.style;
  9346. styleAttr.position = "absolute";
  9347. styleAttr.visibility = "hidden";
  9348. styleAttr.border = "1px solid " + style.color.border;
  9349. styleAttr.color = style.fontColor;
  9350. styleAttr.fontSize = style.fontSize + "px";
  9351. styleAttr.fontFamily = style.fontFace;
  9352. styleAttr.padding = this.padding + "px";
  9353. styleAttr.backgroundColor = style.color.background;
  9354. styleAttr.borderRadius = "3px";
  9355. styleAttr.MozBorderRadius = "3px";
  9356. styleAttr.WebkitBorderRadius = "3px";
  9357. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  9358. styleAttr.whiteSpace = "nowrap";
  9359. this.container.appendChild(this.frame);
  9360. }
  9361. /**
  9362. * @param {number} x Horizontal position of the popup window
  9363. * @param {number} y Vertical position of the popup window
  9364. */
  9365. Popup.prototype.setPosition = function(x, y) {
  9366. this.x = parseInt(x);
  9367. this.y = parseInt(y);
  9368. };
  9369. /**
  9370. * Set the text for the popup window. This can be HTML code
  9371. * @param {string} text
  9372. */
  9373. Popup.prototype.setText = function(text) {
  9374. this.frame.innerHTML = text;
  9375. };
  9376. /**
  9377. * Show the popup window
  9378. * @param {boolean} show Optional. Show or hide the window
  9379. */
  9380. Popup.prototype.show = function (show) {
  9381. if (show === undefined) {
  9382. show = true;
  9383. }
  9384. if (show) {
  9385. var height = this.frame.clientHeight;
  9386. var width = this.frame.clientWidth;
  9387. var maxHeight = this.frame.parentNode.clientHeight;
  9388. var maxWidth = this.frame.parentNode.clientWidth;
  9389. var top = (this.y - height);
  9390. if (top + height + this.padding > maxHeight) {
  9391. top = maxHeight - height - this.padding;
  9392. }
  9393. if (top < this.padding) {
  9394. top = this.padding;
  9395. }
  9396. var left = this.x;
  9397. if (left + width + this.padding > maxWidth) {
  9398. left = maxWidth - width - this.padding;
  9399. }
  9400. if (left < this.padding) {
  9401. left = this.padding;
  9402. }
  9403. this.frame.style.left = left + "px";
  9404. this.frame.style.top = top + "px";
  9405. this.frame.style.visibility = "visible";
  9406. }
  9407. else {
  9408. this.hide();
  9409. }
  9410. };
  9411. /**
  9412. * Hide the popup window
  9413. */
  9414. Popup.prototype.hide = function () {
  9415. this.frame.style.visibility = "hidden";
  9416. };
  9417. /**
  9418. * @class Groups
  9419. * This class can store groups and properties specific for groups.
  9420. */
  9421. function Groups() {
  9422. this.clear();
  9423. this.defaultIndex = 0;
  9424. }
  9425. /**
  9426. * default constants for group colors
  9427. */
  9428. Groups.DEFAULT = [
  9429. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  9430. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  9431. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  9432. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  9433. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  9434. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  9435. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  9436. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  9437. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  9438. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  9439. ];
  9440. /**
  9441. * Clear all groups
  9442. */
  9443. Groups.prototype.clear = function () {
  9444. this.groups = {};
  9445. this.groups.length = function()
  9446. {
  9447. var i = 0;
  9448. for ( var p in this ) {
  9449. if (this.hasOwnProperty(p)) {
  9450. i++;
  9451. }
  9452. }
  9453. return i;
  9454. }
  9455. };
  9456. /**
  9457. * get group properties of a groupname. If groupname is not found, a new group
  9458. * is added.
  9459. * @param {*} groupname Can be a number, string, Date, etc.
  9460. * @return {Object} group The created group, containing all group properties
  9461. */
  9462. Groups.prototype.get = function (groupname) {
  9463. var group = this.groups[groupname];
  9464. if (group == undefined) {
  9465. // create new group
  9466. var index = this.defaultIndex % Groups.DEFAULT.length;
  9467. this.defaultIndex++;
  9468. group = {};
  9469. group.color = Groups.DEFAULT[index];
  9470. this.groups[groupname] = group;
  9471. }
  9472. return group;
  9473. };
  9474. /**
  9475. * Add a custom group style
  9476. * @param {String} groupname
  9477. * @param {Object} style An object containing borderColor,
  9478. * backgroundColor, etc.
  9479. * @return {Object} group The created group object
  9480. */
  9481. Groups.prototype.add = function (groupname, style) {
  9482. this.groups[groupname] = style;
  9483. if (style.color) {
  9484. style.color = util.parseColor(style.color);
  9485. }
  9486. return style;
  9487. };
  9488. /**
  9489. * @class Images
  9490. * This class loads images and keeps them stored.
  9491. */
  9492. function Images() {
  9493. this.images = {};
  9494. this.callback = undefined;
  9495. }
  9496. /**
  9497. * Set an onload callback function. This will be called each time an image
  9498. * is loaded
  9499. * @param {function} callback
  9500. */
  9501. Images.prototype.setOnloadCallback = function(callback) {
  9502. this.callback = callback;
  9503. };
  9504. /**
  9505. *
  9506. * @param {string} url Url of the image
  9507. * @return {Image} img The image object
  9508. */
  9509. Images.prototype.load = function(url) {
  9510. var img = this.images[url];
  9511. if (img == undefined) {
  9512. // create the image
  9513. var images = this;
  9514. img = new Image();
  9515. this.images[url] = img;
  9516. img.onload = function() {
  9517. if (images.callback) {
  9518. images.callback(this);
  9519. }
  9520. };
  9521. img.src = url;
  9522. }
  9523. return img;
  9524. };
  9525. /**
  9526. * Created by Alex on 2/6/14.
  9527. */
  9528. var physicsMixin = {
  9529. /**
  9530. * Toggling barnes Hut calculation on and off.
  9531. *
  9532. * @private
  9533. */
  9534. _toggleBarnesHut: function () {
  9535. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  9536. this._loadSelectedForceSolver();
  9537. this.moving = true;
  9538. this.start();
  9539. },
  9540. /**
  9541. * This loads the node force solver based on the barnes hut or repulsion algorithm
  9542. *
  9543. * @private
  9544. */
  9545. _loadSelectedForceSolver: function () {
  9546. // this overloads the this._calculateNodeForces
  9547. if (this.constants.physics.barnesHut.enabled == true) {
  9548. this._clearMixin(repulsionMixin);
  9549. this._clearMixin(hierarchalRepulsionMixin);
  9550. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  9551. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  9552. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  9553. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  9554. this._loadMixin(barnesHutMixin);
  9555. }
  9556. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  9557. this._clearMixin(barnesHutMixin);
  9558. this._clearMixin(repulsionMixin);
  9559. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  9560. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  9561. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  9562. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  9563. this._loadMixin(hierarchalRepulsionMixin);
  9564. }
  9565. else {
  9566. this._clearMixin(barnesHutMixin);
  9567. this._clearMixin(hierarchalRepulsionMixin);
  9568. this.barnesHutTree = undefined;
  9569. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  9570. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  9571. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  9572. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  9573. this._loadMixin(repulsionMixin);
  9574. }
  9575. },
  9576. /**
  9577. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  9578. * if there is more than one node. If it is just one node, we dont calculate anything.
  9579. *
  9580. * @private
  9581. */
  9582. _initializeForceCalculation: function () {
  9583. // stop calculation if there is only one node
  9584. if (this.nodeIndices.length == 1) {
  9585. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  9586. }
  9587. else {
  9588. // if there are too many nodes on screen, we cluster without repositioning
  9589. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  9590. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  9591. }
  9592. // we now start the force calculation
  9593. this._calculateForces();
  9594. }
  9595. },
  9596. /**
  9597. * Calculate the external forces acting on the nodes
  9598. * Forces are caused by: edges, repulsing forces between nodes, gravity
  9599. * @private
  9600. */
  9601. _calculateForces: function () {
  9602. // Gravity is required to keep separated groups from floating off
  9603. // the forces are reset to zero in this loop by using _setForce instead
  9604. // of _addForce
  9605. this._calculateGravitationalForces();
  9606. this._calculateNodeForces();
  9607. if (this.constants.smoothCurves == true) {
  9608. this._calculateSpringForcesWithSupport();
  9609. }
  9610. else {
  9611. this._calculateSpringForces();
  9612. }
  9613. },
  9614. /**
  9615. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  9616. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  9617. * This function joins the datanodes and invisible (called support) nodes into one object.
  9618. * We do this so we do not contaminate this.nodes with the support nodes.
  9619. *
  9620. * @private
  9621. */
  9622. _updateCalculationNodes: function () {
  9623. if (this.constants.smoothCurves == true) {
  9624. this.calculationNodes = {};
  9625. this.calculationNodeIndices = [];
  9626. for (var nodeId in this.nodes) {
  9627. if (this.nodes.hasOwnProperty(nodeId)) {
  9628. this.calculationNodes[nodeId] = this.nodes[nodeId];
  9629. }
  9630. }
  9631. var supportNodes = this.sectors['support']['nodes'];
  9632. for (var supportNodeId in supportNodes) {
  9633. if (supportNodes.hasOwnProperty(supportNodeId)) {
  9634. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  9635. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  9636. }
  9637. else {
  9638. supportNodes[supportNodeId]._setForce(0, 0);
  9639. }
  9640. }
  9641. }
  9642. for (var idx in this.calculationNodes) {
  9643. if (this.calculationNodes.hasOwnProperty(idx)) {
  9644. this.calculationNodeIndices.push(idx);
  9645. }
  9646. }
  9647. }
  9648. else {
  9649. this.calculationNodes = this.nodes;
  9650. this.calculationNodeIndices = this.nodeIndices;
  9651. }
  9652. },
  9653. /**
  9654. * this function applies the central gravity effect to keep groups from floating off
  9655. *
  9656. * @private
  9657. */
  9658. _calculateGravitationalForces: function () {
  9659. var dx, dy, distance, node, i;
  9660. var nodes = this.calculationNodes;
  9661. var gravity = this.constants.physics.centralGravity;
  9662. var gravityForce = 0;
  9663. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  9664. node = nodes[this.calculationNodeIndices[i]];
  9665. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  9666. // gravity does not apply when we are in a pocket sector
  9667. if (this._sector() == "default" && gravity != 0) {
  9668. dx = -node.x;
  9669. dy = -node.y;
  9670. distance = Math.sqrt(dx * dx + dy * dy);
  9671. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  9672. node.fx = dx * gravityForce;
  9673. node.fy = dy * gravityForce;
  9674. }
  9675. else {
  9676. node.fx = 0;
  9677. node.fy = 0;
  9678. }
  9679. }
  9680. },
  9681. /**
  9682. * this function calculates the effects of the springs in the case of unsmooth curves.
  9683. *
  9684. * @private
  9685. */
  9686. _calculateSpringForces: function () {
  9687. var edgeLength, edge, edgeId;
  9688. var dx, dy, fx, fy, springForce, length;
  9689. var edges = this.edges;
  9690. // forces caused by the edges, modelled as springs
  9691. for (edgeId in edges) {
  9692. if (edges.hasOwnProperty(edgeId)) {
  9693. edge = edges[edgeId];
  9694. if (edge.connected) {
  9695. // only calculate forces if nodes are in the same sector
  9696. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9697. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9698. // this implies that the edges between big clusters are longer
  9699. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  9700. dx = (edge.from.x - edge.to.x);
  9701. dy = (edge.from.y - edge.to.y);
  9702. length = Math.sqrt(dx * dx + dy * dy);
  9703. if (length == 0) {
  9704. length = 0.01;
  9705. }
  9706. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  9707. fx = dx * springForce;
  9708. fy = dy * springForce;
  9709. edge.from.fx += fx;
  9710. edge.from.fy += fy;
  9711. edge.to.fx -= fx;
  9712. edge.to.fy -= fy;
  9713. }
  9714. }
  9715. }
  9716. }
  9717. },
  9718. /**
  9719. * This function calculates the springforces on the nodes, accounting for the support nodes.
  9720. *
  9721. * @private
  9722. */
  9723. _calculateSpringForcesWithSupport: function () {
  9724. var edgeLength, edge, edgeId, combinedClusterSize;
  9725. var edges = this.edges;
  9726. // forces caused by the edges, modelled as springs
  9727. for (edgeId in edges) {
  9728. if (edges.hasOwnProperty(edgeId)) {
  9729. edge = edges[edgeId];
  9730. if (edge.connected) {
  9731. // only calculate forces if nodes are in the same sector
  9732. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9733. if (edge.via != null) {
  9734. var node1 = edge.to;
  9735. var node2 = edge.via;
  9736. var node3 = edge.from;
  9737. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9738. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  9739. // this implies that the edges between big clusters are longer
  9740. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  9741. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  9742. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  9743. }
  9744. }
  9745. }
  9746. }
  9747. }
  9748. },
  9749. /**
  9750. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  9751. *
  9752. * @param node1
  9753. * @param node2
  9754. * @param edgeLength
  9755. * @private
  9756. */
  9757. _calculateSpringForce: function (node1, node2, edgeLength) {
  9758. var dx, dy, fx, fy, springForce, length;
  9759. dx = (node1.x - node2.x);
  9760. dy = (node1.y - node2.y);
  9761. length = Math.sqrt(dx * dx + dy * dy);
  9762. if (length == 0) {
  9763. length = 0.01;
  9764. }
  9765. springForce = this.constants.physics.springConstant * (edgeLength - length) / length;
  9766. fx = dx * springForce;
  9767. fy = dy * springForce;
  9768. node1.fx += fx;
  9769. node1.fy += fy;
  9770. node2.fx -= fx;
  9771. node2.fy -= fy;
  9772. },
  9773. /**
  9774. * Load the HTML for the physics config and bind it
  9775. * @private
  9776. */
  9777. _loadPhysicsConfiguration: function () {
  9778. if (this.physicsConfiguration === undefined) {
  9779. this.backupConstants = {};
  9780. util.copyObject(this.constants, this.backupConstants);
  9781. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  9782. this.physicsConfiguration = document.createElement('div');
  9783. this.physicsConfiguration.className = "PhysicsConfiguration";
  9784. this.physicsConfiguration.innerHTML = '' +
  9785. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  9786. '<tr>' +
  9787. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  9788. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  9789. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  9790. '</tr>' +
  9791. '</table>' +
  9792. '<table id="graph_BH_table" style="display:none">' +
  9793. '<tr><td><b>Barnes Hut</b></td></tr>' +
  9794. '<tr>' +
  9795. '<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>' +
  9796. '</tr>' +
  9797. '<tr>' +
  9798. '<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>' +
  9799. '</tr>' +
  9800. '<tr>' +
  9801. '<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>' +
  9802. '</tr>' +
  9803. '<tr>' +
  9804. '<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>' +
  9805. '</tr>' +
  9806. '<tr>' +
  9807. '<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>' +
  9808. '</tr>' +
  9809. '</table>' +
  9810. '<table id="graph_R_table" style="display:none">' +
  9811. '<tr><td><b>Repulsion</b></td></tr>' +
  9812. '<tr>' +
  9813. '<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>' +
  9814. '</tr>' +
  9815. '<tr>' +
  9816. '<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>' +
  9817. '</tr>' +
  9818. '<tr>' +
  9819. '<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>' +
  9820. '</tr>' +
  9821. '<tr>' +
  9822. '<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>' +
  9823. '</tr>' +
  9824. '<tr>' +
  9825. '<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>' +
  9826. '</tr>' +
  9827. '</table>' +
  9828. '<table id="graph_H_table" style="display:none">' +
  9829. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  9830. '<tr>' +
  9831. '<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>' +
  9832. '</tr>' +
  9833. '<tr>' +
  9834. '<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>' +
  9835. '</tr>' +
  9836. '<tr>' +
  9837. '<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>' +
  9838. '</tr>' +
  9839. '<tr>' +
  9840. '<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>' +
  9841. '</tr>' +
  9842. '<tr>' +
  9843. '<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>' +
  9844. '</tr>' +
  9845. '<tr>' +
  9846. '<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>' +
  9847. '</tr>' +
  9848. '<tr>' +
  9849. '<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>' +
  9850. '</tr>' +
  9851. '<tr>' +
  9852. '<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>' +
  9853. '</tr>' +
  9854. '</table>' +
  9855. '<table><tr><td><b>Options:</b></td></tr>' +
  9856. '<tr>' +
  9857. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  9858. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  9859. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  9860. '</tr>' +
  9861. '</table>'
  9862. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  9863. this.optionsDiv = document.createElement("div");
  9864. this.optionsDiv.style.fontSize = "14px";
  9865. this.optionsDiv.style.fontFamily = "verdana";
  9866. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  9867. var rangeElement;
  9868. rangeElement = document.getElementById('graph_BH_gc');
  9869. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  9870. rangeElement = document.getElementById('graph_BH_cg');
  9871. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  9872. rangeElement = document.getElementById('graph_BH_sc');
  9873. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  9874. rangeElement = document.getElementById('graph_BH_sl');
  9875. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  9876. rangeElement = document.getElementById('graph_BH_damp');
  9877. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  9878. rangeElement = document.getElementById('graph_R_nd');
  9879. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  9880. rangeElement = document.getElementById('graph_R_cg');
  9881. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  9882. rangeElement = document.getElementById('graph_R_sc');
  9883. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  9884. rangeElement = document.getElementById('graph_R_sl');
  9885. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  9886. rangeElement = document.getElementById('graph_R_damp');
  9887. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  9888. rangeElement = document.getElementById('graph_H_nd');
  9889. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  9890. rangeElement = document.getElementById('graph_H_cg');
  9891. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  9892. rangeElement = document.getElementById('graph_H_sc');
  9893. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  9894. rangeElement = document.getElementById('graph_H_sl');
  9895. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  9896. rangeElement = document.getElementById('graph_H_damp');
  9897. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  9898. rangeElement = document.getElementById('graph_H_direction');
  9899. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  9900. rangeElement = document.getElementById('graph_H_levsep');
  9901. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  9902. rangeElement = document.getElementById('graph_H_nspac');
  9903. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  9904. var radioButton1 = document.getElementById("graph_physicsMethod1");
  9905. var radioButton2 = document.getElementById("graph_physicsMethod2");
  9906. var radioButton3 = document.getElementById("graph_physicsMethod3");
  9907. radioButton2.checked = true;
  9908. if (this.constants.physics.barnesHut.enabled) {
  9909. radioButton1.checked = true;
  9910. }
  9911. if (this.constants.hierarchicalLayout.enabled) {
  9912. radioButton3.checked = true;
  9913. }
  9914. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  9915. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  9916. var graph_generateOptions = document.getElementById("graph_generateOptions");
  9917. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  9918. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  9919. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  9920. if (this.constants.smoothCurves == true) {
  9921. graph_toggleSmooth.style.background = "#A4FF56";
  9922. }
  9923. else {
  9924. graph_toggleSmooth.style.background = "#FF8532";
  9925. }
  9926. switchConfigurations.apply(this);
  9927. radioButton1.onchange = switchConfigurations.bind(this);
  9928. radioButton2.onchange = switchConfigurations.bind(this);
  9929. radioButton3.onchange = switchConfigurations.bind(this);
  9930. }
  9931. },
  9932. /**
  9933. * This overwrites the this.constants.
  9934. *
  9935. * @param constantsVariableName
  9936. * @param value
  9937. * @private
  9938. */
  9939. _overWriteGraphConstants: function (constantsVariableName, value) {
  9940. var nameArray = constantsVariableName.split("_");
  9941. if (nameArray.length == 1) {
  9942. this.constants[nameArray[0]] = value;
  9943. }
  9944. else if (nameArray.length == 2) {
  9945. this.constants[nameArray[0]][nameArray[1]] = value;
  9946. }
  9947. else if (nameArray.length == 3) {
  9948. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  9949. }
  9950. }
  9951. };
  9952. /**
  9953. * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
  9954. */
  9955. function graphToggleSmoothCurves () {
  9956. this.constants.smoothCurves = !this.constants.smoothCurves;
  9957. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  9958. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  9959. else {graph_toggleSmooth.style.background = "#FF8532";}
  9960. this._configureSmoothCurves(false);
  9961. };
  9962. /**
  9963. * this function is used to scramble the nodes
  9964. *
  9965. */
  9966. function graphRepositionNodes () {
  9967. for (var nodeId in this.calculationNodes) {
  9968. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  9969. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  9970. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  9971. }
  9972. }
  9973. if (this.constants.hierarchicalLayout.enabled == true) {
  9974. this._setupHierarchicalLayout();
  9975. }
  9976. else {
  9977. this.repositionNodes();
  9978. }
  9979. this.moving = true;
  9980. this.start();
  9981. };
  9982. /**
  9983. * this is used to generate an options file from the playing with physics system.
  9984. */
  9985. function graphGenerateOptions () {
  9986. var options = "No options are required, default values used.";
  9987. var optionsSpecific = [];
  9988. var radioButton1 = document.getElementById("graph_physicsMethod1");
  9989. var radioButton2 = document.getElementById("graph_physicsMethod2");
  9990. if (radioButton1.checked == true) {
  9991. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  9992. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  9993. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  9994. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  9995. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  9996. if (optionsSpecific.length != 0) {
  9997. options = "var options = {";
  9998. options += "physics: {barnesHut: {";
  9999. for (var i = 0; i < optionsSpecific.length; i++) {
  10000. options += optionsSpecific[i];
  10001. if (i < optionsSpecific.length - 1) {
  10002. options += ", "
  10003. }
  10004. }
  10005. options += '}}'
  10006. }
  10007. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10008. if (optionsSpecific.length == 0) {options = "var options = {";}
  10009. else {options += ", "}
  10010. options += "smoothCurves: " + this.constants.smoothCurves;
  10011. }
  10012. if (options != "No options are required, default values used.") {
  10013. options += '};'
  10014. }
  10015. }
  10016. else if (radioButton2.checked == true) {
  10017. options = "var options = {";
  10018. options += "physics: {barnesHut: {enabled: false}";
  10019. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  10020. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10021. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10022. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10023. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10024. if (optionsSpecific.length != 0) {
  10025. options += ", repulsion: {";
  10026. for (var i = 0; i < optionsSpecific.length; i++) {
  10027. options += optionsSpecific[i];
  10028. if (i < optionsSpecific.length - 1) {
  10029. options += ", "
  10030. }
  10031. }
  10032. options += '}}'
  10033. }
  10034. if (optionsSpecific.length == 0) {options += "}"}
  10035. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10036. options += ", smoothCurves: " + this.constants.smoothCurves;
  10037. }
  10038. options += '};'
  10039. }
  10040. else {
  10041. options = "var options = {";
  10042. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  10043. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10044. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10045. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10046. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10047. if (optionsSpecific.length != 0) {
  10048. options += "physics: {hierarchicalRepulsion: {";
  10049. for (var i = 0; i < optionsSpecific.length; i++) {
  10050. options += optionsSpecific[i];
  10051. if (i < optionsSpecific.length - 1) {
  10052. options += ", ";
  10053. }
  10054. }
  10055. options += '}},';
  10056. }
  10057. options += 'hierarchicalLayout: {';
  10058. optionsSpecific = [];
  10059. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  10060. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  10061. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  10062. if (optionsSpecific.length != 0) {
  10063. for (var i = 0; i < optionsSpecific.length; i++) {
  10064. options += optionsSpecific[i];
  10065. if (i < optionsSpecific.length - 1) {
  10066. options += ", "
  10067. }
  10068. }
  10069. options += '}'
  10070. }
  10071. else {
  10072. options += "enabled:true}";
  10073. }
  10074. options += '};'
  10075. }
  10076. this.optionsDiv.innerHTML = options;
  10077. };
  10078. /**
  10079. * this is used to switch between barnesHut, repulsion and hierarchical.
  10080. *
  10081. */
  10082. function switchConfigurations () {
  10083. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  10084. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  10085. var tableId = "graph_" + radioButton + "_table";
  10086. var table = document.getElementById(tableId);
  10087. table.style.display = "block";
  10088. for (var i = 0; i < ids.length; i++) {
  10089. if (ids[i] != tableId) {
  10090. table = document.getElementById(ids[i]);
  10091. table.style.display = "none";
  10092. }
  10093. }
  10094. this._restoreNodes();
  10095. if (radioButton == "R") {
  10096. this.constants.hierarchicalLayout.enabled = false;
  10097. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10098. this.constants.physics.barnesHut.enabled = false;
  10099. }
  10100. else if (radioButton == "H") {
  10101. if (this.constants.hierarchicalLayout.enabled == false) {
  10102. this.constants.hierarchicalLayout.enabled = true;
  10103. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10104. this.constants.physics.barnesHut.enabled = false;
  10105. this._setupHierarchicalLayout();
  10106. }
  10107. }
  10108. else {
  10109. this.constants.hierarchicalLayout.enabled = false;
  10110. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10111. this.constants.physics.barnesHut.enabled = true;
  10112. }
  10113. this._loadSelectedForceSolver();
  10114. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10115. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  10116. else {graph_toggleSmooth.style.background = "#FF8532";}
  10117. this.moving = true;
  10118. this.start();
  10119. }
  10120. /**
  10121. * this generates the ranges depending on the iniital values.
  10122. *
  10123. * @param id
  10124. * @param map
  10125. * @param constantsVariableName
  10126. */
  10127. function showValueOfRange (id,map,constantsVariableName) {
  10128. var valueId = id + "_value";
  10129. var rangeValue = document.getElementById(id).value;
  10130. if (map instanceof Array) {
  10131. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  10132. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  10133. }
  10134. else {
  10135. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  10136. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  10137. }
  10138. if (constantsVariableName == "hierarchicalLayout_direction" ||
  10139. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  10140. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  10141. this._setupHierarchicalLayout();
  10142. }
  10143. this.moving = true;
  10144. this.start();
  10145. };
  10146. /**
  10147. * Created by Alex on 2/10/14.
  10148. */
  10149. var hierarchalRepulsionMixin = {
  10150. /**
  10151. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10152. * This field is linearly approximated.
  10153. *
  10154. * @private
  10155. */
  10156. _calculateNodeForces: function () {
  10157. var dx, dy, distance, fx, fy, combinedClusterSize,
  10158. repulsingForce, node1, node2, i, j;
  10159. var nodes = this.calculationNodes;
  10160. var nodeIndices = this.calculationNodeIndices;
  10161. // approximation constants
  10162. var b = 5;
  10163. var a_base = 0.5 * -b;
  10164. // repulsing forces between nodes
  10165. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  10166. var minimumDistance = nodeDistance;
  10167. // we loop from i over all but the last entree in the array
  10168. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10169. for (i = 0; i < nodeIndices.length - 1; i++) {
  10170. node1 = nodes[nodeIndices[i]];
  10171. for (j = i + 1; j < nodeIndices.length; j++) {
  10172. node2 = nodes[nodeIndices[j]];
  10173. dx = node2.x - node1.x;
  10174. dy = node2.y - node1.y;
  10175. distance = Math.sqrt(dx * dx + dy * dy);
  10176. var a = a_base / minimumDistance;
  10177. if (distance < 2 * minimumDistance) {
  10178. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10179. // normalize force with
  10180. if (distance == 0) {
  10181. distance = 0.01;
  10182. }
  10183. else {
  10184. repulsingForce = repulsingForce / distance;
  10185. }
  10186. fx = dx * repulsingForce;
  10187. fy = dy * repulsingForce;
  10188. node1.fx -= fx;
  10189. node1.fy -= fy;
  10190. node2.fx += fx;
  10191. node2.fy += fy;
  10192. }
  10193. }
  10194. }
  10195. }
  10196. };
  10197. /**
  10198. * Created by Alex on 2/10/14.
  10199. */
  10200. var barnesHutMixin = {
  10201. /**
  10202. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  10203. * The Barnes Hut method is used to speed up this N-body simulation.
  10204. *
  10205. * @private
  10206. */
  10207. _calculateNodeForces : function() {
  10208. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  10209. var node;
  10210. var nodes = this.calculationNodes;
  10211. var nodeIndices = this.calculationNodeIndices;
  10212. var nodeCount = nodeIndices.length;
  10213. this._formBarnesHutTree(nodes,nodeIndices);
  10214. var barnesHutTree = this.barnesHutTree;
  10215. // place the nodes one by one recursively
  10216. for (var i = 0; i < nodeCount; i++) {
  10217. node = nodes[nodeIndices[i]];
  10218. // starting with root is irrelevant, it never passes the BarnesHut condition
  10219. this._getForceContribution(barnesHutTree.root.children.NW,node);
  10220. this._getForceContribution(barnesHutTree.root.children.NE,node);
  10221. this._getForceContribution(barnesHutTree.root.children.SW,node);
  10222. this._getForceContribution(barnesHutTree.root.children.SE,node);
  10223. }
  10224. }
  10225. },
  10226. /**
  10227. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  10228. * If a region contains a single node, we check if it is not itself, then we apply the force.
  10229. *
  10230. * @param parentBranch
  10231. * @param node
  10232. * @private
  10233. */
  10234. _getForceContribution : function(parentBranch,node) {
  10235. // we get no force contribution from an empty region
  10236. if (parentBranch.childrenCount > 0) {
  10237. var dx,dy,distance;
  10238. // get the distance from the center of mass to the node.
  10239. dx = parentBranch.centerOfMass.x - node.x;
  10240. dy = parentBranch.centerOfMass.y - node.y;
  10241. distance = Math.sqrt(dx * dx + dy * dy);
  10242. // BarnesHut condition
  10243. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  10244. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  10245. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  10246. // duplicate code to reduce function calls to speed up program
  10247. if (distance == 0) {
  10248. distance = 0.1*Math.random();
  10249. dx = distance;
  10250. }
  10251. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10252. var fx = dx * gravityForce;
  10253. var fy = dy * gravityForce;
  10254. node.fx += fx;
  10255. node.fy += fy;
  10256. }
  10257. else {
  10258. // Did not pass the condition, go into children if available
  10259. if (parentBranch.childrenCount == 4) {
  10260. this._getForceContribution(parentBranch.children.NW,node);
  10261. this._getForceContribution(parentBranch.children.NE,node);
  10262. this._getForceContribution(parentBranch.children.SW,node);
  10263. this._getForceContribution(parentBranch.children.SE,node);
  10264. }
  10265. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  10266. if (parentBranch.children.data.id != node.id) { // if it is not self
  10267. // duplicate code to reduce function calls to speed up program
  10268. if (distance == 0) {
  10269. distance = 0.5*Math.random();
  10270. dx = distance;
  10271. }
  10272. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10273. var fx = dx * gravityForce;
  10274. var fy = dy * gravityForce;
  10275. node.fx += fx;
  10276. node.fy += fy;
  10277. }
  10278. }
  10279. }
  10280. }
  10281. },
  10282. /**
  10283. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  10284. *
  10285. * @param nodes
  10286. * @param nodeIndices
  10287. * @private
  10288. */
  10289. _formBarnesHutTree : function(nodes,nodeIndices) {
  10290. var node;
  10291. var nodeCount = nodeIndices.length;
  10292. var minX = Number.MAX_VALUE,
  10293. minY = Number.MAX_VALUE,
  10294. maxX =-Number.MAX_VALUE,
  10295. maxY =-Number.MAX_VALUE;
  10296. // get the range of the nodes
  10297. for (var i = 0; i < nodeCount; i++) {
  10298. var x = nodes[nodeIndices[i]].x;
  10299. var y = nodes[nodeIndices[i]].y;
  10300. if (x < minX) { minX = x; }
  10301. if (x > maxX) { maxX = x; }
  10302. if (y < minY) { minY = y; }
  10303. if (y > maxY) { maxY = y; }
  10304. }
  10305. // make the range a square
  10306. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  10307. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  10308. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  10309. var minimumTreeSize = 1e-5;
  10310. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  10311. var halfRootSize = 0.5 * rootSize;
  10312. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  10313. // construct the barnesHutTree
  10314. var barnesHutTree = {root:{
  10315. centerOfMass:{x:0,y:0}, // Center of Mass
  10316. mass:0,
  10317. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  10318. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  10319. size: rootSize,
  10320. calcSize: 1 / rootSize,
  10321. children: {data:null},
  10322. maxWidth: 0,
  10323. level: 0,
  10324. childrenCount: 4
  10325. }};
  10326. this._splitBranch(barnesHutTree.root);
  10327. // place the nodes one by one recursively
  10328. for (i = 0; i < nodeCount; i++) {
  10329. node = nodes[nodeIndices[i]];
  10330. this._placeInTree(barnesHutTree.root,node);
  10331. }
  10332. // make global
  10333. this.barnesHutTree = barnesHutTree
  10334. },
  10335. /**
  10336. * this updates the mass of a branch. this is increased by adding a node.
  10337. *
  10338. * @param parentBranch
  10339. * @param node
  10340. * @private
  10341. */
  10342. _updateBranchMass : function(parentBranch, node) {
  10343. var totalMass = parentBranch.mass + node.mass;
  10344. var totalMassInv = 1/totalMass;
  10345. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  10346. parentBranch.centerOfMass.x *= totalMassInv;
  10347. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  10348. parentBranch.centerOfMass.y *= totalMassInv;
  10349. parentBranch.mass = totalMass;
  10350. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  10351. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  10352. },
  10353. /**
  10354. * determine in which branch the node will be placed.
  10355. *
  10356. * @param parentBranch
  10357. * @param node
  10358. * @param skipMassUpdate
  10359. * @private
  10360. */
  10361. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  10362. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  10363. // update the mass of the branch.
  10364. this._updateBranchMass(parentBranch,node);
  10365. }
  10366. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  10367. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  10368. this._placeInRegion(parentBranch,node,"NW");
  10369. }
  10370. else { // in SW
  10371. this._placeInRegion(parentBranch,node,"SW");
  10372. }
  10373. }
  10374. else { // in NE or SE
  10375. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  10376. this._placeInRegion(parentBranch,node,"NE");
  10377. }
  10378. else { // in SE
  10379. this._placeInRegion(parentBranch,node,"SE");
  10380. }
  10381. }
  10382. },
  10383. /**
  10384. * actually place the node in a region (or branch)
  10385. *
  10386. * @param parentBranch
  10387. * @param node
  10388. * @param region
  10389. * @private
  10390. */
  10391. _placeInRegion : function(parentBranch,node,region) {
  10392. switch (parentBranch.children[region].childrenCount) {
  10393. case 0: // place node here
  10394. parentBranch.children[region].children.data = node;
  10395. parentBranch.children[region].childrenCount = 1;
  10396. this._updateBranchMass(parentBranch.children[region],node);
  10397. break;
  10398. case 1: // convert into children
  10399. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  10400. // we move one node a pixel and we do not put it in the tree.
  10401. if (parentBranch.children[region].children.data.x == node.x &&
  10402. parentBranch.children[region].children.data.y == node.y) {
  10403. node.x += Math.random();
  10404. node.y += Math.random();
  10405. }
  10406. else {
  10407. this._splitBranch(parentBranch.children[region]);
  10408. this._placeInTree(parentBranch.children[region],node);
  10409. }
  10410. break;
  10411. case 4: // place in branch
  10412. this._placeInTree(parentBranch.children[region],node);
  10413. break;
  10414. }
  10415. },
  10416. /**
  10417. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  10418. * after the split is complete.
  10419. *
  10420. * @param parentBranch
  10421. * @private
  10422. */
  10423. _splitBranch : function(parentBranch) {
  10424. // if the branch is filled with a node, replace the node in the new subset.
  10425. var containedNode = null;
  10426. if (parentBranch.childrenCount == 1) {
  10427. containedNode = parentBranch.children.data;
  10428. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  10429. }
  10430. parentBranch.childrenCount = 4;
  10431. parentBranch.children.data = null;
  10432. this._insertRegion(parentBranch,"NW");
  10433. this._insertRegion(parentBranch,"NE");
  10434. this._insertRegion(parentBranch,"SW");
  10435. this._insertRegion(parentBranch,"SE");
  10436. if (containedNode != null) {
  10437. this._placeInTree(parentBranch,containedNode);
  10438. }
  10439. },
  10440. /**
  10441. * This function subdivides the region into four new segments.
  10442. * Specifically, this inserts a single new segment.
  10443. * It fills the children section of the parentBranch
  10444. *
  10445. * @param parentBranch
  10446. * @param region
  10447. * @param parentRange
  10448. * @private
  10449. */
  10450. _insertRegion : function(parentBranch, region) {
  10451. var minX,maxX,minY,maxY;
  10452. var childSize = 0.5 * parentBranch.size;
  10453. switch (region) {
  10454. case "NW":
  10455. minX = parentBranch.range.minX;
  10456. maxX = parentBranch.range.minX + childSize;
  10457. minY = parentBranch.range.minY;
  10458. maxY = parentBranch.range.minY + childSize;
  10459. break;
  10460. case "NE":
  10461. minX = parentBranch.range.minX + childSize;
  10462. maxX = parentBranch.range.maxX;
  10463. minY = parentBranch.range.minY;
  10464. maxY = parentBranch.range.minY + childSize;
  10465. break;
  10466. case "SW":
  10467. minX = parentBranch.range.minX;
  10468. maxX = parentBranch.range.minX + childSize;
  10469. minY = parentBranch.range.minY + childSize;
  10470. maxY = parentBranch.range.maxY;
  10471. break;
  10472. case "SE":
  10473. minX = parentBranch.range.minX + childSize;
  10474. maxX = parentBranch.range.maxX;
  10475. minY = parentBranch.range.minY + childSize;
  10476. maxY = parentBranch.range.maxY;
  10477. break;
  10478. }
  10479. parentBranch.children[region] = {
  10480. centerOfMass:{x:0,y:0},
  10481. mass:0,
  10482. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  10483. size: 0.5 * parentBranch.size,
  10484. calcSize: 2 * parentBranch.calcSize,
  10485. children: {data:null},
  10486. maxWidth: 0,
  10487. level: parentBranch.level+1,
  10488. childrenCount: 0
  10489. };
  10490. },
  10491. /**
  10492. * This function is for debugging purposed, it draws the tree.
  10493. *
  10494. * @param ctx
  10495. * @param color
  10496. * @private
  10497. */
  10498. _drawTree : function(ctx,color) {
  10499. if (this.barnesHutTree !== undefined) {
  10500. ctx.lineWidth = 1;
  10501. this._drawBranch(this.barnesHutTree.root,ctx,color);
  10502. }
  10503. },
  10504. /**
  10505. * This function is for debugging purposes. It draws the branches recursively.
  10506. *
  10507. * @param branch
  10508. * @param ctx
  10509. * @param color
  10510. * @private
  10511. */
  10512. _drawBranch : function(branch,ctx,color) {
  10513. if (color === undefined) {
  10514. color = "#FF0000";
  10515. }
  10516. if (branch.childrenCount == 4) {
  10517. this._drawBranch(branch.children.NW,ctx);
  10518. this._drawBranch(branch.children.NE,ctx);
  10519. this._drawBranch(branch.children.SE,ctx);
  10520. this._drawBranch(branch.children.SW,ctx);
  10521. }
  10522. ctx.strokeStyle = color;
  10523. ctx.beginPath();
  10524. ctx.moveTo(branch.range.minX,branch.range.minY);
  10525. ctx.lineTo(branch.range.maxX,branch.range.minY);
  10526. ctx.stroke();
  10527. ctx.beginPath();
  10528. ctx.moveTo(branch.range.maxX,branch.range.minY);
  10529. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  10530. ctx.stroke();
  10531. ctx.beginPath();
  10532. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  10533. ctx.lineTo(branch.range.minX,branch.range.maxY);
  10534. ctx.stroke();
  10535. ctx.beginPath();
  10536. ctx.moveTo(branch.range.minX,branch.range.maxY);
  10537. ctx.lineTo(branch.range.minX,branch.range.minY);
  10538. ctx.stroke();
  10539. /*
  10540. if (branch.mass > 0) {
  10541. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  10542. ctx.stroke();
  10543. }
  10544. */
  10545. }
  10546. };
  10547. /**
  10548. * Created by Alex on 2/10/14.
  10549. */
  10550. var repulsionMixin = {
  10551. /**
  10552. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10553. * This field is linearly approximated.
  10554. *
  10555. * @private
  10556. */
  10557. _calculateNodeForces: function () {
  10558. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  10559. repulsingForce, node1, node2, i, j;
  10560. var nodes = this.calculationNodes;
  10561. var nodeIndices = this.calculationNodeIndices;
  10562. // approximation constants
  10563. var a_base = -2 / 3;
  10564. var b = 4 / 3;
  10565. // repulsing forces between nodes
  10566. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  10567. var minimumDistance = nodeDistance;
  10568. // we loop from i over all but the last entree in the array
  10569. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10570. for (i = 0; i < nodeIndices.length - 1; i++) {
  10571. node1 = nodes[nodeIndices[i]];
  10572. for (j = i + 1; j < nodeIndices.length; j++) {
  10573. node2 = nodes[nodeIndices[j]];
  10574. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  10575. dx = node2.x - node1.x;
  10576. dy = node2.y - node1.y;
  10577. distance = Math.sqrt(dx * dx + dy * dy);
  10578. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  10579. var a = a_base / minimumDistance;
  10580. if (distance < 2 * minimumDistance) {
  10581. if (distance < 0.5 * minimumDistance) {
  10582. repulsingForce = 1.0;
  10583. }
  10584. else {
  10585. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10586. }
  10587. // amplify the repulsion for clusters.
  10588. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  10589. repulsingForce = repulsingForce / distance;
  10590. fx = dx * repulsingForce;
  10591. fy = dy * repulsingForce;
  10592. node1.fx -= fx;
  10593. node1.fy -= fy;
  10594. node2.fx += fx;
  10595. node2.fy += fy;
  10596. }
  10597. }
  10598. }
  10599. }
  10600. };
  10601. var HierarchicalLayoutMixin = {
  10602. _resetLevels : function() {
  10603. for (var nodeId in this.nodes) {
  10604. if (this.nodes.hasOwnProperty(nodeId)) {
  10605. var node = this.nodes[nodeId];
  10606. if (node.preassignedLevel == false) {
  10607. node.level = -1;
  10608. }
  10609. }
  10610. }
  10611. },
  10612. /**
  10613. * This is the main function to layout the nodes in a hierarchical way.
  10614. * It checks if the node details are supplied correctly
  10615. *
  10616. * @private
  10617. */
  10618. _setupHierarchicalLayout : function() {
  10619. if (this.constants.hierarchicalLayout.enabled == true) {
  10620. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  10621. this.constants.hierarchicalLayout.levelSeparation *= -1;
  10622. }
  10623. else {
  10624. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  10625. }
  10626. // get the size of the largest hubs and check if the user has defined a level for a node.
  10627. var hubsize = 0;
  10628. var node, nodeId;
  10629. var definedLevel = false;
  10630. var undefinedLevel = false;
  10631. for (nodeId in this.nodes) {
  10632. if (this.nodes.hasOwnProperty(nodeId)) {
  10633. node = this.nodes[nodeId];
  10634. if (node.level != -1) {
  10635. definedLevel = true;
  10636. }
  10637. else {
  10638. undefinedLevel = true;
  10639. }
  10640. if (hubsize < node.edges.length) {
  10641. hubsize = node.edges.length;
  10642. }
  10643. }
  10644. }
  10645. // if the user defined some levels but not all, alert and run without hierarchical layout
  10646. if (undefinedLevel == true && definedLevel == true) {
  10647. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  10648. this.zoomExtent(true,this.constants.clustering.enabled);
  10649. if (!this.constants.clustering.enabled) {
  10650. this.start();
  10651. }
  10652. }
  10653. else {
  10654. // setup the system to use hierarchical method.
  10655. this._changeConstants();
  10656. // define levels if undefined by the users. Based on hubsize
  10657. if (undefinedLevel == true) {
  10658. this._determineLevels(hubsize);
  10659. }
  10660. // check the distribution of the nodes per level.
  10661. var distribution = this._getDistribution();
  10662. // place the nodes on the canvas. This also stablilizes the system.
  10663. this._placeNodesByHierarchy(distribution);
  10664. // start the simulation.
  10665. this.start();
  10666. }
  10667. }
  10668. },
  10669. /**
  10670. * This function places the nodes on the canvas based on the hierarchial distribution.
  10671. *
  10672. * @param {Object} distribution | obtained by the function this._getDistribution()
  10673. * @private
  10674. */
  10675. _placeNodesByHierarchy : function(distribution) {
  10676. var nodeId, node;
  10677. // start placing all the level 0 nodes first. Then recursively position their branches.
  10678. for (nodeId in distribution[0].nodes) {
  10679. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  10680. node = distribution[0].nodes[nodeId];
  10681. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10682. if (node.xFixed) {
  10683. node.x = distribution[0].minPos;
  10684. node.xFixed = false;
  10685. distribution[0].minPos += distribution[0].nodeSpacing;
  10686. }
  10687. }
  10688. else {
  10689. if (node.yFixed) {
  10690. node.y = distribution[0].minPos;
  10691. node.yFixed = false;
  10692. distribution[0].minPos += distribution[0].nodeSpacing;
  10693. }
  10694. }
  10695. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  10696. }
  10697. }
  10698. // stabilize the system after positioning. This function calls zoomExtent.
  10699. this._stabilize();
  10700. },
  10701. /**
  10702. * This function get the distribution of levels based on hubsize
  10703. *
  10704. * @returns {Object}
  10705. * @private
  10706. */
  10707. _getDistribution : function() {
  10708. var distribution = {};
  10709. var nodeId, node, level;
  10710. // 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.
  10711. // the fix of X is removed after the x value has been set.
  10712. for (nodeId in this.nodes) {
  10713. if (this.nodes.hasOwnProperty(nodeId)) {
  10714. node = this.nodes[nodeId];
  10715. node.xFixed = true;
  10716. node.yFixed = true;
  10717. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10718. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10719. }
  10720. else {
  10721. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10722. }
  10723. if (!distribution.hasOwnProperty(node.level)) {
  10724. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  10725. }
  10726. distribution[node.level].amount += 1;
  10727. distribution[node.level].nodes[node.id] = node;
  10728. }
  10729. }
  10730. // determine the largest amount of nodes of all levels
  10731. var maxCount = 0;
  10732. for (level in distribution) {
  10733. if (distribution.hasOwnProperty(level)) {
  10734. if (maxCount < distribution[level].amount) {
  10735. maxCount = distribution[level].amount;
  10736. }
  10737. }
  10738. }
  10739. // set the initial position and spacing of each nodes accordingly
  10740. for (level in distribution) {
  10741. if (distribution.hasOwnProperty(level)) {
  10742. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  10743. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  10744. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  10745. }
  10746. }
  10747. return distribution;
  10748. },
  10749. /**
  10750. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  10751. *
  10752. * @param hubsize
  10753. * @private
  10754. */
  10755. _determineLevels : function(hubsize) {
  10756. var nodeId, node;
  10757. // determine hubs
  10758. for (nodeId in this.nodes) {
  10759. if (this.nodes.hasOwnProperty(nodeId)) {
  10760. node = this.nodes[nodeId];
  10761. if (node.edges.length == hubsize) {
  10762. node.level = 0;
  10763. }
  10764. }
  10765. }
  10766. // branch from hubs
  10767. for (nodeId in this.nodes) {
  10768. if (this.nodes.hasOwnProperty(nodeId)) {
  10769. node = this.nodes[nodeId];
  10770. if (node.level == 0) {
  10771. this._setLevel(1,node.edges,node.id);
  10772. }
  10773. }
  10774. }
  10775. },
  10776. /**
  10777. * Since hierarchical layout does not support:
  10778. * - smooth curves (based on the physics),
  10779. * - clustering (based on dynamic node counts)
  10780. *
  10781. * We disable both features so there will be no problems.
  10782. *
  10783. * @private
  10784. */
  10785. _changeConstants : function() {
  10786. this.constants.clustering.enabled = false;
  10787. this.constants.physics.barnesHut.enabled = false;
  10788. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10789. this._loadSelectedForceSolver();
  10790. this.constants.smoothCurves = false;
  10791. this._configureSmoothCurves();
  10792. },
  10793. /**
  10794. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  10795. * on a X position that ensures there will be no overlap.
  10796. *
  10797. * @param edges
  10798. * @param parentId
  10799. * @param distribution
  10800. * @param parentLevel
  10801. * @private
  10802. */
  10803. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  10804. for (var i = 0; i < edges.length; i++) {
  10805. var childNode = null;
  10806. if (edges[i].toId == parentId) {
  10807. childNode = edges[i].from;
  10808. }
  10809. else {
  10810. childNode = edges[i].to;
  10811. }
  10812. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  10813. var nodeMoved = false;
  10814. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10815. if (childNode.xFixed && childNode.level > parentLevel) {
  10816. childNode.xFixed = false;
  10817. childNode.x = distribution[childNode.level].minPos;
  10818. nodeMoved = true;
  10819. }
  10820. }
  10821. else {
  10822. if (childNode.yFixed && childNode.level > parentLevel) {
  10823. childNode.yFixed = false;
  10824. childNode.y = distribution[childNode.level].minPos;
  10825. nodeMoved = true;
  10826. }
  10827. }
  10828. if (nodeMoved == true) {
  10829. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  10830. if (childNode.edges.length > 1) {
  10831. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  10832. }
  10833. }
  10834. }
  10835. },
  10836. /**
  10837. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  10838. *
  10839. * @param level
  10840. * @param edges
  10841. * @param parentId
  10842. * @private
  10843. */
  10844. _setLevel : function(level, edges, parentId) {
  10845. for (var i = 0; i < edges.length; i++) {
  10846. var childNode = null;
  10847. if (edges[i].toId == parentId) {
  10848. childNode = edges[i].from;
  10849. }
  10850. else {
  10851. childNode = edges[i].to;
  10852. }
  10853. if (childNode.level == -1 || childNode.level > level) {
  10854. childNode.level = level;
  10855. if (edges.length > 1) {
  10856. this._setLevel(level+1, childNode.edges, childNode.id);
  10857. }
  10858. }
  10859. }
  10860. },
  10861. /**
  10862. * Unfix nodes
  10863. *
  10864. * @private
  10865. */
  10866. _restoreNodes : function() {
  10867. for (nodeId in this.nodes) {
  10868. if (this.nodes.hasOwnProperty(nodeId)) {
  10869. this.nodes[nodeId].xFixed = false;
  10870. this.nodes[nodeId].yFixed = false;
  10871. }
  10872. }
  10873. }
  10874. };
  10875. /**
  10876. * Created by Alex on 2/4/14.
  10877. */
  10878. var manipulationMixin = {
  10879. /**
  10880. * clears the toolbar div element of children
  10881. *
  10882. * @private
  10883. */
  10884. _clearManipulatorBar : function() {
  10885. while (this.manipulationDiv.hasChildNodes()) {
  10886. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  10887. }
  10888. },
  10889. /**
  10890. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  10891. * these functions to their original functionality, we saved them in this.cachedFunctions.
  10892. * This function restores these functions to their original function.
  10893. *
  10894. * @private
  10895. */
  10896. _restoreOverloadedFunctions : function() {
  10897. for (var functionName in this.cachedFunctions) {
  10898. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  10899. this[functionName] = this.cachedFunctions[functionName];
  10900. }
  10901. }
  10902. },
  10903. /**
  10904. * Enable or disable edit-mode.
  10905. *
  10906. * @private
  10907. */
  10908. _toggleEditMode : function() {
  10909. this.editMode = !this.editMode;
  10910. var toolbar = document.getElementById("graph-manipulationDiv");
  10911. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  10912. var editModeDiv = document.getElementById("graph-manipulation-editMode");
  10913. if (this.editMode == true) {
  10914. toolbar.style.display="block";
  10915. closeDiv.style.display="block";
  10916. editModeDiv.style.display="none";
  10917. closeDiv.onclick = this._toggleEditMode.bind(this);
  10918. }
  10919. else {
  10920. toolbar.style.display="none";
  10921. closeDiv.style.display="none";
  10922. editModeDiv.style.display="block";
  10923. closeDiv.onclick = null;
  10924. }
  10925. this._createManipulatorBar()
  10926. },
  10927. /**
  10928. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  10929. *
  10930. * @private
  10931. */
  10932. _createManipulatorBar : function() {
  10933. // remove bound functions
  10934. if (this.boundFunction) {
  10935. this.off('select', this.boundFunction);
  10936. }
  10937. // restore overloaded functions
  10938. this._restoreOverloadedFunctions();
  10939. // resume calculation
  10940. this.freezeSimulation = false;
  10941. // reset global variables
  10942. this.blockConnectingEdgeSelection = false;
  10943. this.forceAppendSelection = false;
  10944. if (this.editMode == true) {
  10945. while (this.manipulationDiv.hasChildNodes()) {
  10946. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  10947. }
  10948. // add the icons to the manipulator div
  10949. this.manipulationDiv.innerHTML = "" +
  10950. "<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
  10951. "<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  10952. "<div class='graph-seperatorLine'></div>" +
  10953. "<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
  10954. "<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  10955. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  10956. this.manipulationDiv.innerHTML += "" +
  10957. "<div class='graph-seperatorLine'></div>" +
  10958. "<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
  10959. "<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  10960. }
  10961. if (this._selectionIsEmpty() == false) {
  10962. this.manipulationDiv.innerHTML += "" +
  10963. "<div class='graph-seperatorLine'></div>" +
  10964. "<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
  10965. "<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  10966. }
  10967. // bind the icons
  10968. var addNodeButton = document.getElementById("graph-manipulate-addNode");
  10969. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  10970. var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
  10971. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  10972. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  10973. var editButton = document.getElementById("graph-manipulate-editNode");
  10974. editButton.onclick = this._editNode.bind(this);
  10975. }
  10976. if (this._selectionIsEmpty() == false) {
  10977. var deleteButton = document.getElementById("graph-manipulate-delete");
  10978. deleteButton.onclick = this._deleteSelected.bind(this);
  10979. }
  10980. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  10981. closeDiv.onclick = this._toggleEditMode.bind(this);
  10982. this.boundFunction = this._createManipulatorBar.bind(this);
  10983. this.on('select', this.boundFunction);
  10984. }
  10985. else {
  10986. this.editModeDiv.innerHTML = "" +
  10987. "<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
  10988. "<span class='graph-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  10989. var editModeButton = document.getElementById("graph-manipulate-editModeButton");
  10990. editModeButton.onclick = this._toggleEditMode.bind(this);
  10991. }
  10992. },
  10993. /**
  10994. * Create the toolbar for adding Nodes
  10995. *
  10996. * @private
  10997. */
  10998. _createAddNodeToolbar : function() {
  10999. // clear the toolbar
  11000. this._clearManipulatorBar();
  11001. if (this.boundFunction) {
  11002. this.off('select', this.boundFunction);
  11003. }
  11004. // create the toolbar contents
  11005. this.manipulationDiv.innerHTML = "" +
  11006. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11007. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11008. "<div class='graph-seperatorLine'></div>" +
  11009. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11010. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  11011. // bind the icon
  11012. var backButton = document.getElementById("graph-manipulate-back");
  11013. backButton.onclick = this._createManipulatorBar.bind(this);
  11014. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11015. this.boundFunction = this._addNode.bind(this);
  11016. this.on('select', this.boundFunction);
  11017. },
  11018. /**
  11019. * create the toolbar to connect nodes
  11020. *
  11021. * @private
  11022. */
  11023. _createAddEdgeToolbar : function() {
  11024. // clear the toolbar
  11025. this._clearManipulatorBar();
  11026. this._unselectAll(true);
  11027. this.freezeSimulation = true;
  11028. if (this.boundFunction) {
  11029. this.off('select', this.boundFunction);
  11030. }
  11031. this._unselectAll();
  11032. this.forceAppendSelection = false;
  11033. this.blockConnectingEdgeSelection = true;
  11034. this.manipulationDiv.innerHTML = "" +
  11035. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11036. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11037. "<div class='graph-seperatorLine'></div>" +
  11038. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11039. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
  11040. // bind the icon
  11041. var backButton = document.getElementById("graph-manipulate-back");
  11042. backButton.onclick = this._createManipulatorBar.bind(this);
  11043. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11044. this.boundFunction = this._handleConnect.bind(this);
  11045. this.on('select', this.boundFunction);
  11046. // temporarily overload functions
  11047. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  11048. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  11049. this._handleTouch = this._handleConnect;
  11050. this._handleOnRelease = this._finishConnect;
  11051. // redraw to show the unselect
  11052. this._redraw();
  11053. },
  11054. /**
  11055. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  11056. * to walk the user through the process.
  11057. *
  11058. * @private
  11059. */
  11060. _handleConnect : function(pointer) {
  11061. if (this._getSelectedNodeCount() == 0) {
  11062. var node = this._getNodeAt(pointer);
  11063. if (node != null) {
  11064. if (node.clusterSize > 1) {
  11065. alert("Cannot create edges to a cluster.")
  11066. }
  11067. else {
  11068. this._selectObject(node,false);
  11069. // create a node the temporary line can look at
  11070. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  11071. this.sectors['support']['nodes']['targetNode'].x = node.x;
  11072. this.sectors['support']['nodes']['targetNode'].y = node.y;
  11073. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  11074. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  11075. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  11076. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  11077. // create a temporary edge
  11078. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  11079. this.edges['connectionEdge'].from = node;
  11080. this.edges['connectionEdge'].connected = true;
  11081. this.edges['connectionEdge'].smooth = true;
  11082. this.edges['connectionEdge'].selected = true;
  11083. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  11084. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  11085. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  11086. this._handleOnDrag = function(event) {
  11087. var pointer = this._getPointer(event.gesture.center);
  11088. this.sectors['support']['nodes']['targetNode'].x = this._canvasToX(pointer.x);
  11089. this.sectors['support']['nodes']['targetNode'].y = this._canvasToY(pointer.y);
  11090. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._canvasToX(pointer.x) + this.edges['connectionEdge'].from.x);
  11091. this.sectors['support']['nodes']['targetViaNode'].y = this._canvasToY(pointer.y);
  11092. };
  11093. this.moving = true;
  11094. this.start();
  11095. }
  11096. }
  11097. }
  11098. },
  11099. _finishConnect : function(pointer) {
  11100. if (this._getSelectedNodeCount() == 1) {
  11101. // restore the drag function
  11102. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  11103. delete this.cachedFunctions["_handleOnDrag"];
  11104. // remember the edge id
  11105. var connectFromId = this.edges['connectionEdge'].fromId;
  11106. // remove the temporary nodes and edge
  11107. delete this.edges['connectionEdge'];
  11108. delete this.sectors['support']['nodes']['targetNode'];
  11109. delete this.sectors['support']['nodes']['targetViaNode'];
  11110. var node = this._getNodeAt(pointer);
  11111. if (node != null) {
  11112. if (node.clusterSize > 1) {
  11113. alert("Cannot create edges to a cluster.")
  11114. }
  11115. else {
  11116. this._createEdge(connectFromId,node.id);
  11117. this._createManipulatorBar();
  11118. }
  11119. }
  11120. this._unselectAll();
  11121. }
  11122. },
  11123. /**
  11124. * Adds a node on the specified location
  11125. */
  11126. _addNode : function() {
  11127. if (this._selectionIsEmpty() && this.editMode == true) {
  11128. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  11129. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  11130. if (this.triggerFunctions.add) {
  11131. if (this.triggerFunctions.add.length == 2) {
  11132. var me = this;
  11133. this.triggerFunctions.add(defaultData, function(finalizedData) {
  11134. me.nodesData.add(finalizedData);
  11135. me._createManipulatorBar();
  11136. me.moving = true;
  11137. me.start();
  11138. });
  11139. }
  11140. else {
  11141. alert(this.constants.labels['addError']);
  11142. this._createManipulatorBar();
  11143. this.moving = true;
  11144. this.start();
  11145. }
  11146. }
  11147. else {
  11148. this.nodesData.add(defaultData);
  11149. this._createManipulatorBar();
  11150. this.moving = true;
  11151. this.start();
  11152. }
  11153. }
  11154. },
  11155. /**
  11156. * connect two nodes with a new edge.
  11157. *
  11158. * @private
  11159. */
  11160. _createEdge : function(sourceNodeId,targetNodeId) {
  11161. if (this.editMode == true) {
  11162. var defaultData = {from:sourceNodeId, to:targetNodeId};
  11163. if (this.triggerFunctions.connect) {
  11164. if (this.triggerFunctions.connect.length == 2) {
  11165. var me = this;
  11166. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  11167. me.edgesData.add(finalizedData);
  11168. me.moving = true;
  11169. me.start();
  11170. });
  11171. }
  11172. else {
  11173. alert(this.constants.labels["linkError"]);
  11174. this.moving = true;
  11175. this.start();
  11176. }
  11177. }
  11178. else {
  11179. this.edgesData.add(defaultData);
  11180. this.moving = true;
  11181. this.start();
  11182. }
  11183. }
  11184. },
  11185. /**
  11186. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  11187. *
  11188. * @private
  11189. */
  11190. _editNode : function() {
  11191. if (this.triggerFunctions.edit && this.editMode == true) {
  11192. var node = this._getSelectedNode();
  11193. var data = {id:node.id,
  11194. label: node.label,
  11195. group: node.group,
  11196. shape: node.shape,
  11197. color: {
  11198. background:node.color.background,
  11199. border:node.color.border,
  11200. highlight: {
  11201. background:node.color.highlight.background,
  11202. border:node.color.highlight.border
  11203. }
  11204. }};
  11205. if (this.triggerFunctions.edit.length == 2) {
  11206. var me = this;
  11207. this.triggerFunctions.edit(data, function (finalizedData) {
  11208. me.nodesData.update(finalizedData);
  11209. me._createManipulatorBar();
  11210. me.moving = true;
  11211. me.start();
  11212. });
  11213. }
  11214. else {
  11215. alert(this.constants.labels["editError"]);
  11216. }
  11217. }
  11218. else {
  11219. alert(this.constants.labels["editBoundError"]);
  11220. }
  11221. },
  11222. /**
  11223. * delete everything in the selection
  11224. *
  11225. * @private
  11226. */
  11227. _deleteSelected : function() {
  11228. if (!this._selectionIsEmpty() && this.editMode == true) {
  11229. if (!this._clusterInSelection()) {
  11230. var selectedNodes = this.getSelectedNodes();
  11231. var selectedEdges = this.getSelectedEdges();
  11232. if (this.triggerFunctions.del) {
  11233. var me = this;
  11234. var data = {nodes: selectedNodes, edges: selectedEdges};
  11235. if (this.triggerFunctions.del.length = 2) {
  11236. this.triggerFunctions.del(data, function (finalizedData) {
  11237. me.edgesData.remove(finalizedData.edges);
  11238. me.nodesData.remove(finalizedData.nodes);
  11239. me._unselectAll();
  11240. me.moving = true;
  11241. me.start();
  11242. });
  11243. }
  11244. else {
  11245. alert(this.constants.labels["deleteError"])
  11246. }
  11247. }
  11248. else {
  11249. this.edgesData.remove(selectedEdges);
  11250. this.nodesData.remove(selectedNodes);
  11251. this._unselectAll();
  11252. this.moving = true;
  11253. this.start();
  11254. }
  11255. }
  11256. else {
  11257. alert(this.constants.labels["deleteClusterError"]);
  11258. }
  11259. }
  11260. }
  11261. };
  11262. /**
  11263. * Creation of the SectorMixin var.
  11264. *
  11265. * This contains all the functions the Graph object can use to employ the sector system.
  11266. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  11267. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  11268. *
  11269. * Alex de Mulder
  11270. * 21-01-2013
  11271. */
  11272. var SectorMixin = {
  11273. /**
  11274. * This function is only called by the setData function of the Graph object.
  11275. * This loads the global references into the active sector. This initializes the sector.
  11276. *
  11277. * @private
  11278. */
  11279. _putDataInSector : function() {
  11280. this.sectors["active"][this._sector()].nodes = this.nodes;
  11281. this.sectors["active"][this._sector()].edges = this.edges;
  11282. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  11283. },
  11284. /**
  11285. * /**
  11286. * This function sets the global references to nodes, edges and nodeIndices back to
  11287. * those of the supplied (active) sector. If a type is defined, do the specific type
  11288. *
  11289. * @param {String} sectorId
  11290. * @param {String} [sectorType] | "active" or "frozen"
  11291. * @private
  11292. */
  11293. _switchToSector : function(sectorId, sectorType) {
  11294. if (sectorType === undefined || sectorType == "active") {
  11295. this._switchToActiveSector(sectorId);
  11296. }
  11297. else {
  11298. this._switchToFrozenSector(sectorId);
  11299. }
  11300. },
  11301. /**
  11302. * This function sets the global references to nodes, edges and nodeIndices back to
  11303. * those of the supplied active sector.
  11304. *
  11305. * @param sectorId
  11306. * @private
  11307. */
  11308. _switchToActiveSector : function(sectorId) {
  11309. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  11310. this.nodes = this.sectors["active"][sectorId]["nodes"];
  11311. this.edges = this.sectors["active"][sectorId]["edges"];
  11312. },
  11313. /**
  11314. * This function sets the global references to nodes, edges and nodeIndices back to
  11315. * those of the supplied active sector.
  11316. *
  11317. * @param sectorId
  11318. * @private
  11319. */
  11320. _switchToSupportSector : function() {
  11321. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  11322. this.nodes = this.sectors["support"]["nodes"];
  11323. this.edges = this.sectors["support"]["edges"];
  11324. },
  11325. /**
  11326. * This function sets the global references to nodes, edges and nodeIndices back to
  11327. * those of the supplied frozen sector.
  11328. *
  11329. * @param sectorId
  11330. * @private
  11331. */
  11332. _switchToFrozenSector : function(sectorId) {
  11333. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  11334. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  11335. this.edges = this.sectors["frozen"][sectorId]["edges"];
  11336. },
  11337. /**
  11338. * This function sets the global references to nodes, edges and nodeIndices back to
  11339. * those of the currently active sector.
  11340. *
  11341. * @private
  11342. */
  11343. _loadLatestSector : function() {
  11344. this._switchToSector(this._sector());
  11345. },
  11346. /**
  11347. * This function returns the currently active sector Id
  11348. *
  11349. * @returns {String}
  11350. * @private
  11351. */
  11352. _sector : function() {
  11353. return this.activeSector[this.activeSector.length-1];
  11354. },
  11355. /**
  11356. * This function returns the previously active sector Id
  11357. *
  11358. * @returns {String}
  11359. * @private
  11360. */
  11361. _previousSector : function() {
  11362. if (this.activeSector.length > 1) {
  11363. return this.activeSector[this.activeSector.length-2];
  11364. }
  11365. else {
  11366. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  11367. }
  11368. },
  11369. /**
  11370. * We add the active sector at the end of the this.activeSector array
  11371. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  11372. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  11373. *
  11374. * @param newId
  11375. * @private
  11376. */
  11377. _setActiveSector : function(newId) {
  11378. this.activeSector.push(newId);
  11379. },
  11380. /**
  11381. * We remove the currently active sector id from the active sector stack. This happens when
  11382. * we reactivate the previously active sector
  11383. *
  11384. * @private
  11385. */
  11386. _forgetLastSector : function() {
  11387. this.activeSector.pop();
  11388. },
  11389. /**
  11390. * This function creates a new active sector with the supplied newId. This newId
  11391. * is the expanding node id.
  11392. *
  11393. * @param {String} newId | Id of the new active sector
  11394. * @private
  11395. */
  11396. _createNewSector : function(newId) {
  11397. // create the new sector
  11398. this.sectors["active"][newId] = {"nodes":{},
  11399. "edges":{},
  11400. "nodeIndices":[],
  11401. "formationScale": this.scale,
  11402. "drawingNode": undefined};
  11403. // create the new sector render node. This gives visual feedback that you are in a new sector.
  11404. this.sectors["active"][newId]['drawingNode'] = new Node(
  11405. {id:newId,
  11406. color: {
  11407. background: "#eaefef",
  11408. border: "495c5e"
  11409. }
  11410. },{},{},this.constants);
  11411. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  11412. },
  11413. /**
  11414. * This function removes the currently active sector. This is called when we create a new
  11415. * active sector.
  11416. *
  11417. * @param {String} sectorId | Id of the active sector that will be removed
  11418. * @private
  11419. */
  11420. _deleteActiveSector : function(sectorId) {
  11421. delete this.sectors["active"][sectorId];
  11422. },
  11423. /**
  11424. * This function removes the currently active sector. This is called when we reactivate
  11425. * the previously active sector.
  11426. *
  11427. * @param {String} sectorId | Id of the active sector that will be removed
  11428. * @private
  11429. */
  11430. _deleteFrozenSector : function(sectorId) {
  11431. delete this.sectors["frozen"][sectorId];
  11432. },
  11433. /**
  11434. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  11435. * We copy the references, then delete the active entree.
  11436. *
  11437. * @param sectorId
  11438. * @private
  11439. */
  11440. _freezeSector : function(sectorId) {
  11441. // we move the set references from the active to the frozen stack.
  11442. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  11443. // we have moved the sector data into the frozen set, we now remove it from the active set
  11444. this._deleteActiveSector(sectorId);
  11445. },
  11446. /**
  11447. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  11448. * object to the "active" object.
  11449. *
  11450. * @param sectorId
  11451. * @private
  11452. */
  11453. _activateSector : function(sectorId) {
  11454. // we move the set references from the frozen to the active stack.
  11455. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  11456. // we have moved the sector data into the active set, we now remove it from the frozen stack
  11457. this._deleteFrozenSector(sectorId);
  11458. },
  11459. /**
  11460. * This function merges the data from the currently active sector with a frozen sector. This is used
  11461. * in the process of reverting back to the previously active sector.
  11462. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  11463. * upon the creation of a new active sector.
  11464. *
  11465. * @param sectorId
  11466. * @private
  11467. */
  11468. _mergeThisWithFrozen : function(sectorId) {
  11469. // copy all nodes
  11470. for (var nodeId in this.nodes) {
  11471. if (this.nodes.hasOwnProperty(nodeId)) {
  11472. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  11473. }
  11474. }
  11475. // copy all edges (if not fully clustered, else there are no edges)
  11476. for (var edgeId in this.edges) {
  11477. if (this.edges.hasOwnProperty(edgeId)) {
  11478. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  11479. }
  11480. }
  11481. // merge the nodeIndices
  11482. for (var i = 0; i < this.nodeIndices.length; i++) {
  11483. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  11484. }
  11485. },
  11486. /**
  11487. * This clusters the sector to one cluster. It was a single cluster before this process started so
  11488. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  11489. *
  11490. * @private
  11491. */
  11492. _collapseThisToSingleCluster : function() {
  11493. this.clusterToFit(1,false);
  11494. },
  11495. /**
  11496. * We create a new active sector from the node that we want to open.
  11497. *
  11498. * @param node
  11499. * @private
  11500. */
  11501. _addSector : function(node) {
  11502. // this is the currently active sector
  11503. var sector = this._sector();
  11504. // // this should allow me to select nodes from a frozen set.
  11505. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  11506. // console.log("the node is part of the active sector");
  11507. // }
  11508. // else {
  11509. // console.log("I dont know what happened!!");
  11510. // }
  11511. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  11512. delete this.nodes[node.id];
  11513. var unqiueIdentifier = util.randomUUID();
  11514. // we fully freeze the currently active sector
  11515. this._freezeSector(sector);
  11516. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  11517. this._createNewSector(unqiueIdentifier);
  11518. // we add the active sector to the sectors array to be able to revert these steps later on
  11519. this._setActiveSector(unqiueIdentifier);
  11520. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  11521. this._switchToSector(this._sector());
  11522. // finally we add the node we removed from our previous active sector to the new active sector
  11523. this.nodes[node.id] = node;
  11524. },
  11525. /**
  11526. * We close the sector that is currently open and revert back to the one before.
  11527. * If the active sector is the "default" sector, nothing happens.
  11528. *
  11529. * @private
  11530. */
  11531. _collapseSector : function() {
  11532. // the currently active sector
  11533. var sector = this._sector();
  11534. // we cannot collapse the default sector
  11535. if (sector != "default") {
  11536. if ((this.nodeIndices.length == 1) ||
  11537. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11538. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11539. var previousSector = this._previousSector();
  11540. // we collapse the sector back to a single cluster
  11541. this._collapseThisToSingleCluster();
  11542. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  11543. // This previous sector is the one we will reactivate
  11544. this._mergeThisWithFrozen(previousSector);
  11545. // the previously active (frozen) sector now has all the data from the currently active sector.
  11546. // we can now delete the active sector.
  11547. this._deleteActiveSector(sector);
  11548. // we activate the previously active (and currently frozen) sector.
  11549. this._activateSector(previousSector);
  11550. // we load the references from the newly active sector into the global references
  11551. this._switchToSector(previousSector);
  11552. // we forget the previously active sector because we reverted to the one before
  11553. this._forgetLastSector();
  11554. // finally, we update the node index list.
  11555. this._updateNodeIndexList();
  11556. // we refresh the list with calulation nodes and calculation node indices.
  11557. this._updateCalculationNodes();
  11558. }
  11559. }
  11560. },
  11561. /**
  11562. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11563. *
  11564. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11565. * | we dont pass the function itself because then the "this" is the window object
  11566. * | instead of the Graph object
  11567. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11568. * @private
  11569. */
  11570. _doInAllActiveSectors : function(runFunction,argument) {
  11571. if (argument === undefined) {
  11572. for (var sector in this.sectors["active"]) {
  11573. if (this.sectors["active"].hasOwnProperty(sector)) {
  11574. // switch the global references to those of this sector
  11575. this._switchToActiveSector(sector);
  11576. this[runFunction]();
  11577. }
  11578. }
  11579. }
  11580. else {
  11581. for (var sector in this.sectors["active"]) {
  11582. if (this.sectors["active"].hasOwnProperty(sector)) {
  11583. // switch the global references to those of this sector
  11584. this._switchToActiveSector(sector);
  11585. var args = Array.prototype.splice.call(arguments, 1);
  11586. if (args.length > 1) {
  11587. this[runFunction](args[0],args[1]);
  11588. }
  11589. else {
  11590. this[runFunction](argument);
  11591. }
  11592. }
  11593. }
  11594. }
  11595. // we revert the global references back to our active sector
  11596. this._loadLatestSector();
  11597. },
  11598. /**
  11599. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11600. *
  11601. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11602. * | we dont pass the function itself because then the "this" is the window object
  11603. * | instead of the Graph object
  11604. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11605. * @private
  11606. */
  11607. _doInSupportSector : function(runFunction,argument) {
  11608. if (argument === undefined) {
  11609. this._switchToSupportSector();
  11610. this[runFunction]();
  11611. }
  11612. else {
  11613. this._switchToSupportSector();
  11614. var args = Array.prototype.splice.call(arguments, 1);
  11615. if (args.length > 1) {
  11616. this[runFunction](args[0],args[1]);
  11617. }
  11618. else {
  11619. this[runFunction](argument);
  11620. }
  11621. }
  11622. // we revert the global references back to our active sector
  11623. this._loadLatestSector();
  11624. },
  11625. /**
  11626. * This runs a function in all frozen sectors. This is used in the _redraw().
  11627. *
  11628. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11629. * | we don't pass the function itself because then the "this" is the window object
  11630. * | instead of the Graph object
  11631. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11632. * @private
  11633. */
  11634. _doInAllFrozenSectors : function(runFunction,argument) {
  11635. if (argument === undefined) {
  11636. for (var sector in this.sectors["frozen"]) {
  11637. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11638. // switch the global references to those of this sector
  11639. this._switchToFrozenSector(sector);
  11640. this[runFunction]();
  11641. }
  11642. }
  11643. }
  11644. else {
  11645. for (var sector in this.sectors["frozen"]) {
  11646. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11647. // switch the global references to those of this sector
  11648. this._switchToFrozenSector(sector);
  11649. var args = Array.prototype.splice.call(arguments, 1);
  11650. if (args.length > 1) {
  11651. this[runFunction](args[0],args[1]);
  11652. }
  11653. else {
  11654. this[runFunction](argument);
  11655. }
  11656. }
  11657. }
  11658. }
  11659. this._loadLatestSector();
  11660. },
  11661. /**
  11662. * This runs a function in all sectors. This is used in the _redraw().
  11663. *
  11664. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11665. * | we don't pass the function itself because then the "this" is the window object
  11666. * | instead of the Graph object
  11667. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11668. * @private
  11669. */
  11670. _doInAllSectors : function(runFunction,argument) {
  11671. var args = Array.prototype.splice.call(arguments, 1);
  11672. if (argument === undefined) {
  11673. this._doInAllActiveSectors(runFunction);
  11674. this._doInAllFrozenSectors(runFunction);
  11675. }
  11676. else {
  11677. if (args.length > 1) {
  11678. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  11679. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  11680. }
  11681. else {
  11682. this._doInAllActiveSectors(runFunction,argument);
  11683. this._doInAllFrozenSectors(runFunction,argument);
  11684. }
  11685. }
  11686. },
  11687. /**
  11688. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  11689. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  11690. *
  11691. * @private
  11692. */
  11693. _clearNodeIndexList : function() {
  11694. var sector = this._sector();
  11695. this.sectors["active"][sector]["nodeIndices"] = [];
  11696. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  11697. },
  11698. /**
  11699. * Draw the encompassing sector node
  11700. *
  11701. * @param ctx
  11702. * @param sectorType
  11703. * @private
  11704. */
  11705. _drawSectorNodes : function(ctx,sectorType) {
  11706. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  11707. for (var sector in this.sectors[sectorType]) {
  11708. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  11709. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  11710. this._switchToSector(sector,sectorType);
  11711. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  11712. for (var nodeId in this.nodes) {
  11713. if (this.nodes.hasOwnProperty(nodeId)) {
  11714. node = this.nodes[nodeId];
  11715. node.resize(ctx);
  11716. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  11717. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  11718. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  11719. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  11720. }
  11721. }
  11722. node = this.sectors[sectorType][sector]["drawingNode"];
  11723. node.x = 0.5 * (maxX + minX);
  11724. node.y = 0.5 * (maxY + minY);
  11725. node.width = 2 * (node.x - minX);
  11726. node.height = 2 * (node.y - minY);
  11727. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  11728. node.setScale(this.scale);
  11729. node._drawCircle(ctx);
  11730. }
  11731. }
  11732. }
  11733. },
  11734. _drawAllSectorNodes : function(ctx) {
  11735. this._drawSectorNodes(ctx,"frozen");
  11736. this._drawSectorNodes(ctx,"active");
  11737. this._loadLatestSector();
  11738. }
  11739. };
  11740. /**
  11741. * Creation of the ClusterMixin var.
  11742. *
  11743. * This contains all the functions the Graph object can use to employ clustering
  11744. *
  11745. * Alex de Mulder
  11746. * 21-01-2013
  11747. */
  11748. var ClusterMixin = {
  11749. /**
  11750. * This is only called in the constructor of the graph object
  11751. *
  11752. */
  11753. startWithClustering : function() {
  11754. // cluster if the data set is big
  11755. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  11756. // updates the lables after clustering
  11757. this.updateLabels();
  11758. // this is called here because if clusterin is disabled, the start and stabilize are called in
  11759. // the setData function.
  11760. if (this.stabilize) {
  11761. this._stabilize();
  11762. }
  11763. this.start();
  11764. },
  11765. /**
  11766. * This function clusters until the initialMaxNodes has been reached
  11767. *
  11768. * @param {Number} maxNumberOfNodes
  11769. * @param {Boolean} reposition
  11770. */
  11771. clusterToFit : function(maxNumberOfNodes, reposition) {
  11772. var numberOfNodes = this.nodeIndices.length;
  11773. var maxLevels = 50;
  11774. var level = 0;
  11775. // we first cluster the hubs, then we pull in the outliers, repeat
  11776. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  11777. if (level % 3 == 0) {
  11778. this.forceAggregateHubs(true);
  11779. this.normalizeClusterLevels();
  11780. }
  11781. else {
  11782. this.increaseClusterLevel(); // this also includes a cluster normalization
  11783. }
  11784. numberOfNodes = this.nodeIndices.length;
  11785. level += 1;
  11786. }
  11787. // after the clustering we reposition the nodes to reduce the initial chaos
  11788. if (level > 0 && reposition == true) {
  11789. this.repositionNodes();
  11790. }
  11791. this._updateCalculationNodes();
  11792. },
  11793. /**
  11794. * This function can be called to open up a specific cluster. It is only called by
  11795. * It will unpack the cluster back one level.
  11796. *
  11797. * @param node | Node object: cluster to open.
  11798. */
  11799. openCluster : function(node) {
  11800. var isMovingBeforeClustering = this.moving;
  11801. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  11802. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  11803. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  11804. this._addSector(node);
  11805. var level = 0;
  11806. // we decluster until we reach a decent number of nodes
  11807. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  11808. this.decreaseClusterLevel();
  11809. level += 1;
  11810. }
  11811. }
  11812. else {
  11813. this._expandClusterNode(node,false,true);
  11814. // update the index list, dynamic edges and labels
  11815. this._updateNodeIndexList();
  11816. this._updateDynamicEdges();
  11817. this._updateCalculationNodes();
  11818. this.updateLabels();
  11819. }
  11820. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11821. if (this.moving != isMovingBeforeClustering) {
  11822. this.start();
  11823. }
  11824. },
  11825. /**
  11826. * This calls the updateClustes with default arguments
  11827. */
  11828. updateClustersDefault : function() {
  11829. if (this.constants.clustering.enabled == true) {
  11830. this.updateClusters(0,false,false);
  11831. }
  11832. },
  11833. /**
  11834. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  11835. * be clustered with their connected node. This can be repeated as many times as needed.
  11836. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  11837. */
  11838. increaseClusterLevel : function() {
  11839. this.updateClusters(-1,false,true);
  11840. },
  11841. /**
  11842. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  11843. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  11844. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  11845. */
  11846. decreaseClusterLevel : function() {
  11847. this.updateClusters(1,false,true);
  11848. },
  11849. /**
  11850. * This is the main clustering function. It clusters and declusters on zoom or forced
  11851. * This function clusters on zoom, it can be called with a predefined zoom direction
  11852. * If out, check if we can form clusters, if in, check if we can open clusters.
  11853. * This function is only called from _zoom()
  11854. *
  11855. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  11856. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  11857. * @param {Boolean} force | enabled or disable forcing
  11858. * @param {Boolean} doNotStart | if true do not call start
  11859. *
  11860. */
  11861. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  11862. var isMovingBeforeClustering = this.moving;
  11863. var amountOfNodes = this.nodeIndices.length;
  11864. // on zoom out collapse the sector if the scale is at the level the sector was made
  11865. if (this.previousScale > this.scale && zoomDirection == 0) {
  11866. this._collapseSector();
  11867. }
  11868. // check if we zoom in or out
  11869. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  11870. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  11871. // outer nodes determines if it is being clustered
  11872. this._formClusters(force);
  11873. }
  11874. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  11875. if (force == true) {
  11876. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  11877. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  11878. this._openClusters(recursive,force);
  11879. }
  11880. else {
  11881. // if a cluster takes up a set percentage of the active window
  11882. this._openClustersBySize();
  11883. }
  11884. }
  11885. this._updateNodeIndexList();
  11886. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  11887. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  11888. this._aggregateHubs(force);
  11889. this._updateNodeIndexList();
  11890. }
  11891. // we now reduce chains.
  11892. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  11893. this.handleChains();
  11894. this._updateNodeIndexList();
  11895. }
  11896. this.previousScale = this.scale;
  11897. // rest of the update the index list, dynamic edges and labels
  11898. this._updateDynamicEdges();
  11899. this.updateLabels();
  11900. // if a cluster was formed, we increase the clusterSession
  11901. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  11902. this.clusterSession += 1;
  11903. // if clusters have been made, we normalize the cluster level
  11904. this.normalizeClusterLevels();
  11905. }
  11906. if (doNotStart == false || doNotStart === undefined) {
  11907. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11908. if (this.moving != isMovingBeforeClustering) {
  11909. this.start();
  11910. }
  11911. }
  11912. this._updateCalculationNodes();
  11913. },
  11914. /**
  11915. * This function handles the chains. It is called on every updateClusters().
  11916. */
  11917. handleChains : function() {
  11918. // after clustering we check how many chains there are
  11919. var chainPercentage = this._getChainFraction();
  11920. if (chainPercentage > this.constants.clustering.chainThreshold) {
  11921. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  11922. }
  11923. },
  11924. /**
  11925. * this functions starts clustering by hubs
  11926. * The minimum hub threshold is set globally
  11927. *
  11928. * @private
  11929. */
  11930. _aggregateHubs : function(force) {
  11931. this._getHubSize();
  11932. this._formClustersByHub(force,false);
  11933. },
  11934. /**
  11935. * This function is fired by keypress. It forces hubs to form.
  11936. *
  11937. */
  11938. forceAggregateHubs : function(doNotStart) {
  11939. var isMovingBeforeClustering = this.moving;
  11940. var amountOfNodes = this.nodeIndices.length;
  11941. this._aggregateHubs(true);
  11942. // update the index list, dynamic edges and labels
  11943. this._updateNodeIndexList();
  11944. this._updateDynamicEdges();
  11945. this.updateLabels();
  11946. // if a cluster was formed, we increase the clusterSession
  11947. if (this.nodeIndices.length != amountOfNodes) {
  11948. this.clusterSession += 1;
  11949. }
  11950. if (doNotStart == false || doNotStart === undefined) {
  11951. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11952. if (this.moving != isMovingBeforeClustering) {
  11953. this.start();
  11954. }
  11955. }
  11956. },
  11957. /**
  11958. * If a cluster takes up more than a set percentage of the screen, open the cluster
  11959. *
  11960. * @private
  11961. */
  11962. _openClustersBySize : function() {
  11963. for (var nodeId in this.nodes) {
  11964. if (this.nodes.hasOwnProperty(nodeId)) {
  11965. var node = this.nodes[nodeId];
  11966. if (node.inView() == true) {
  11967. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11968. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11969. this.openCluster(node);
  11970. }
  11971. }
  11972. }
  11973. }
  11974. },
  11975. /**
  11976. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  11977. * has to be opened based on the current zoom level.
  11978. *
  11979. * @private
  11980. */
  11981. _openClusters : function(recursive,force) {
  11982. for (var i = 0; i < this.nodeIndices.length; i++) {
  11983. var node = this.nodes[this.nodeIndices[i]];
  11984. this._expandClusterNode(node,recursive,force);
  11985. this._updateCalculationNodes();
  11986. }
  11987. },
  11988. /**
  11989. * This function checks if a node has to be opened. This is done by checking the zoom level.
  11990. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  11991. * This recursive behaviour is optional and can be set by the recursive argument.
  11992. *
  11993. * @param {Node} parentNode | to check for cluster and expand
  11994. * @param {Boolean} recursive | enabled or disable recursive calling
  11995. * @param {Boolean} force | enabled or disable forcing
  11996. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  11997. * @private
  11998. */
  11999. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  12000. // first check if node is a cluster
  12001. if (parentNode.clusterSize > 1) {
  12002. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  12003. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  12004. openAll = true;
  12005. }
  12006. recursive = openAll ? true : recursive;
  12007. // if the last child has been added on a smaller scale than current scale decluster
  12008. if (parentNode.formationScale < this.scale || force == true) {
  12009. // we will check if any of the contained child nodes should be removed from the cluster
  12010. for (var containedNodeId in parentNode.containedNodes) {
  12011. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  12012. var childNode = parentNode.containedNodes[containedNodeId];
  12013. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  12014. // the largest cluster is the one that comes from outside
  12015. if (force == true) {
  12016. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  12017. || openAll) {
  12018. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12019. }
  12020. }
  12021. else {
  12022. if (this._nodeInActiveArea(parentNode)) {
  12023. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12024. }
  12025. }
  12026. }
  12027. }
  12028. }
  12029. }
  12030. },
  12031. /**
  12032. * ONLY CALLED FROM _expandClusterNode
  12033. *
  12034. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  12035. * the child node from the parent contained_node object and put it back into the global nodes object.
  12036. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  12037. *
  12038. * @param {Node} parentNode | the parent node
  12039. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  12040. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  12041. * With force and recursive both true, the entire cluster is unpacked
  12042. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  12043. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  12044. * @private
  12045. */
  12046. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  12047. var childNode = parentNode.containedNodes[containedNodeId];
  12048. // if child node has been added on smaller scale than current, kick out
  12049. if (childNode.formationScale < this.scale || force == true) {
  12050. // unselect all selected items
  12051. this._unselectAll();
  12052. // put the child node back in the global nodes object
  12053. this.nodes[containedNodeId] = childNode;
  12054. // release the contained edges from this childNode back into the global edges
  12055. this._releaseContainedEdges(parentNode,childNode);
  12056. // reconnect rerouted edges to the childNode
  12057. this._connectEdgeBackToChild(parentNode,childNode);
  12058. // validate all edges in dynamicEdges
  12059. this._validateEdges(parentNode);
  12060. // undo the changes from the clustering operation on the parent node
  12061. parentNode.mass -= childNode.mass;
  12062. parentNode.clusterSize -= childNode.clusterSize;
  12063. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12064. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  12065. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  12066. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  12067. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  12068. // remove node from the list
  12069. delete parentNode.containedNodes[containedNodeId];
  12070. // check if there are other childs with this clusterSession in the parent.
  12071. var othersPresent = false;
  12072. for (var childNodeId in parentNode.containedNodes) {
  12073. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  12074. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  12075. othersPresent = true;
  12076. break;
  12077. }
  12078. }
  12079. }
  12080. // if there are no others, remove the cluster session from the list
  12081. if (othersPresent == false) {
  12082. parentNode.clusterSessions.pop();
  12083. }
  12084. this._repositionBezierNodes(childNode);
  12085. // this._repositionBezierNodes(parentNode);
  12086. // remove the clusterSession from the child node
  12087. childNode.clusterSession = 0;
  12088. // recalculate the size of the node on the next time the node is rendered
  12089. parentNode.clearSizeCache();
  12090. // restart the simulation to reorganise all nodes
  12091. this.moving = true;
  12092. }
  12093. // check if a further expansion step is possible if recursivity is enabled
  12094. if (recursive == true) {
  12095. this._expandClusterNode(childNode,recursive,force,openAll);
  12096. }
  12097. },
  12098. /**
  12099. * position the bezier nodes at the center of the edges
  12100. *
  12101. * @param node
  12102. * @private
  12103. */
  12104. _repositionBezierNodes : function(node) {
  12105. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12106. node.dynamicEdges[i].positionBezierNode();
  12107. }
  12108. },
  12109. /**
  12110. * This function checks if any nodes at the end of their trees have edges below a threshold length
  12111. * This function is called only from updateClusters()
  12112. * forceLevelCollapse ignores the length of the edge and collapses one level
  12113. * This means that a node with only one edge will be clustered with its connected node
  12114. *
  12115. * @private
  12116. * @param {Boolean} force
  12117. */
  12118. _formClusters : function(force) {
  12119. if (force == false) {
  12120. this._formClustersByZoom();
  12121. }
  12122. else {
  12123. this._forceClustersByZoom();
  12124. }
  12125. },
  12126. /**
  12127. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  12128. *
  12129. * @private
  12130. */
  12131. _formClustersByZoom : function() {
  12132. var dx,dy,length,
  12133. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12134. // check if any edges are shorter than minLength and start the clustering
  12135. // the clustering favours the node with the larger mass
  12136. for (var edgeId in this.edges) {
  12137. if (this.edges.hasOwnProperty(edgeId)) {
  12138. var edge = this.edges[edgeId];
  12139. if (edge.connected) {
  12140. if (edge.toId != edge.fromId) {
  12141. dx = (edge.to.x - edge.from.x);
  12142. dy = (edge.to.y - edge.from.y);
  12143. length = Math.sqrt(dx * dx + dy * dy);
  12144. if (length < minLength) {
  12145. // first check which node is larger
  12146. var parentNode = edge.from;
  12147. var childNode = edge.to;
  12148. if (edge.to.mass > edge.from.mass) {
  12149. parentNode = edge.to;
  12150. childNode = edge.from;
  12151. }
  12152. if (childNode.dynamicEdgesLength == 1) {
  12153. this._addToCluster(parentNode,childNode,false);
  12154. }
  12155. else if (parentNode.dynamicEdgesLength == 1) {
  12156. this._addToCluster(childNode,parentNode,false);
  12157. }
  12158. }
  12159. }
  12160. }
  12161. }
  12162. }
  12163. },
  12164. /**
  12165. * This function forces the graph to cluster all nodes with only one connecting edge to their
  12166. * connected node.
  12167. *
  12168. * @private
  12169. */
  12170. _forceClustersByZoom : function() {
  12171. for (var nodeId in this.nodes) {
  12172. // another node could have absorbed this child.
  12173. if (this.nodes.hasOwnProperty(nodeId)) {
  12174. var childNode = this.nodes[nodeId];
  12175. // the edges can be swallowed by another decrease
  12176. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  12177. var edge = childNode.dynamicEdges[0];
  12178. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  12179. // group to the largest node
  12180. if (childNode.id != parentNode.id) {
  12181. if (parentNode.mass > childNode.mass) {
  12182. this._addToCluster(parentNode,childNode,true);
  12183. }
  12184. else {
  12185. this._addToCluster(childNode,parentNode,true);
  12186. }
  12187. }
  12188. }
  12189. }
  12190. }
  12191. },
  12192. /**
  12193. * To keep the nodes of roughly equal size we normalize the cluster levels.
  12194. * This function clusters a node to its smallest connected neighbour.
  12195. *
  12196. * @param node
  12197. * @private
  12198. */
  12199. _clusterToSmallestNeighbour : function(node) {
  12200. var smallestNeighbour = -1;
  12201. var smallestNeighbourNode = null;
  12202. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12203. if (node.dynamicEdges[i] !== undefined) {
  12204. var neighbour = null;
  12205. if (node.dynamicEdges[i].fromId != node.id) {
  12206. neighbour = node.dynamicEdges[i].from;
  12207. }
  12208. else if (node.dynamicEdges[i].toId != node.id) {
  12209. neighbour = node.dynamicEdges[i].to;
  12210. }
  12211. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  12212. smallestNeighbour = neighbour.clusterSessions.length;
  12213. smallestNeighbourNode = neighbour;
  12214. }
  12215. }
  12216. }
  12217. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  12218. this._addToCluster(neighbour, node, true);
  12219. }
  12220. },
  12221. /**
  12222. * This function forms clusters from hubs, it loops over all nodes
  12223. *
  12224. * @param {Boolean} force | Disregard zoom level
  12225. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12226. * @private
  12227. */
  12228. _formClustersByHub : function(force, onlyEqual) {
  12229. // we loop over all nodes in the list
  12230. for (var nodeId in this.nodes) {
  12231. // we check if it is still available since it can be used by the clustering in this loop
  12232. if (this.nodes.hasOwnProperty(nodeId)) {
  12233. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  12234. }
  12235. }
  12236. },
  12237. /**
  12238. * This function forms a cluster from a specific preselected hub node
  12239. *
  12240. * @param {Node} hubNode | the node we will cluster as a hub
  12241. * @param {Boolean} force | Disregard zoom level
  12242. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12243. * @param {Number} [absorptionSizeOffset] |
  12244. * @private
  12245. */
  12246. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  12247. if (absorptionSizeOffset === undefined) {
  12248. absorptionSizeOffset = 0;
  12249. }
  12250. // we decide if the node is a hub
  12251. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  12252. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  12253. // initialize variables
  12254. var dx,dy,length;
  12255. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12256. var allowCluster = false;
  12257. // we create a list of edges because the dynamicEdges change over the course of this loop
  12258. var edgesIdarray = [];
  12259. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  12260. for (var j = 0; j < amountOfInitialEdges; j++) {
  12261. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  12262. }
  12263. // if the hub clustering is not forces, we check if one of the edges connected
  12264. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  12265. if (force == false) {
  12266. allowCluster = false;
  12267. for (j = 0; j < amountOfInitialEdges; j++) {
  12268. var edge = this.edges[edgesIdarray[j]];
  12269. if (edge !== undefined) {
  12270. if (edge.connected) {
  12271. if (edge.toId != edge.fromId) {
  12272. dx = (edge.to.x - edge.from.x);
  12273. dy = (edge.to.y - edge.from.y);
  12274. length = Math.sqrt(dx * dx + dy * dy);
  12275. if (length < minLength) {
  12276. allowCluster = true;
  12277. break;
  12278. }
  12279. }
  12280. }
  12281. }
  12282. }
  12283. }
  12284. // start the clustering if allowed
  12285. if ((!force && allowCluster) || force) {
  12286. // we loop over all edges INITIALLY connected to this hub
  12287. for (j = 0; j < amountOfInitialEdges; j++) {
  12288. edge = this.edges[edgesIdarray[j]];
  12289. // the edge can be clustered by this function in a previous loop
  12290. if (edge !== undefined) {
  12291. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  12292. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  12293. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  12294. (childNode.id != hubNode.id)) {
  12295. this._addToCluster(hubNode,childNode,force);
  12296. }
  12297. }
  12298. }
  12299. }
  12300. }
  12301. },
  12302. /**
  12303. * This function adds the child node to the parent node, creating a cluster if it is not already.
  12304. *
  12305. * @param {Node} parentNode | this is the node that will house the child node
  12306. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  12307. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  12308. * @private
  12309. */
  12310. _addToCluster : function(parentNode, childNode, force) {
  12311. // join child node in the parent node
  12312. parentNode.containedNodes[childNode.id] = childNode;
  12313. // manage all the edges connected to the child and parent nodes
  12314. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  12315. var edge = childNode.dynamicEdges[i];
  12316. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  12317. this._addToContainedEdges(parentNode,childNode,edge);
  12318. }
  12319. else {
  12320. this._connectEdgeToCluster(parentNode,childNode,edge);
  12321. }
  12322. }
  12323. // a contained node has no dynamic edges.
  12324. childNode.dynamicEdges = [];
  12325. // remove circular edges from clusters
  12326. this._containCircularEdgesFromNode(parentNode,childNode);
  12327. // remove the childNode from the global nodes object
  12328. delete this.nodes[childNode.id];
  12329. // update the properties of the child and parent
  12330. var massBefore = parentNode.mass;
  12331. childNode.clusterSession = this.clusterSession;
  12332. parentNode.mass += childNode.mass;
  12333. parentNode.clusterSize += childNode.clusterSize;
  12334. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12335. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  12336. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  12337. parentNode.clusterSessions.push(this.clusterSession);
  12338. }
  12339. // forced clusters only open from screen size and double tap
  12340. if (force == true) {
  12341. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  12342. parentNode.formationScale = 0;
  12343. }
  12344. else {
  12345. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  12346. }
  12347. // recalculate the size of the node on the next time the node is rendered
  12348. parentNode.clearSizeCache();
  12349. // set the pop-out scale for the childnode
  12350. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  12351. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  12352. childNode.clearVelocity();
  12353. // the mass has altered, preservation of energy dictates the velocity to be updated
  12354. parentNode.updateVelocity(massBefore);
  12355. // restart the simulation to reorganise all nodes
  12356. this.moving = true;
  12357. },
  12358. /**
  12359. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  12360. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  12361. * It has to be called if a level is collapsed. It is called by _formClusters().
  12362. * @private
  12363. */
  12364. _updateDynamicEdges : function() {
  12365. for (var i = 0; i < this.nodeIndices.length; i++) {
  12366. var node = this.nodes[this.nodeIndices[i]];
  12367. node.dynamicEdgesLength = node.dynamicEdges.length;
  12368. // this corrects for multiple edges pointing at the same other node
  12369. var correction = 0;
  12370. if (node.dynamicEdgesLength > 1) {
  12371. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  12372. var edgeToId = node.dynamicEdges[j].toId;
  12373. var edgeFromId = node.dynamicEdges[j].fromId;
  12374. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  12375. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  12376. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  12377. correction += 1;
  12378. }
  12379. }
  12380. }
  12381. }
  12382. node.dynamicEdgesLength -= correction;
  12383. }
  12384. },
  12385. /**
  12386. * This adds an edge from the childNode to the contained edges of the parent node
  12387. *
  12388. * @param parentNode | Node object
  12389. * @param childNode | Node object
  12390. * @param edge | Edge object
  12391. * @private
  12392. */
  12393. _addToContainedEdges : function(parentNode, childNode, edge) {
  12394. // create an array object if it does not yet exist for this childNode
  12395. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  12396. parentNode.containedEdges[childNode.id] = []
  12397. }
  12398. // add this edge to the list
  12399. parentNode.containedEdges[childNode.id].push(edge);
  12400. // remove the edge from the global edges object
  12401. delete this.edges[edge.id];
  12402. // remove the edge from the parent object
  12403. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12404. if (parentNode.dynamicEdges[i].id == edge.id) {
  12405. parentNode.dynamicEdges.splice(i,1);
  12406. break;
  12407. }
  12408. }
  12409. },
  12410. /**
  12411. * This function connects an edge that was connected to a child node to the parent node.
  12412. * It keeps track of which nodes it has been connected to with the originalId array.
  12413. *
  12414. * @param {Node} parentNode | Node object
  12415. * @param {Node} childNode | Node object
  12416. * @param {Edge} edge | Edge object
  12417. * @private
  12418. */
  12419. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  12420. // handle circular edges
  12421. if (edge.toId == edge.fromId) {
  12422. this._addToContainedEdges(parentNode, childNode, edge);
  12423. }
  12424. else {
  12425. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  12426. edge.originalToId.push(childNode.id);
  12427. edge.to = parentNode;
  12428. edge.toId = parentNode.id;
  12429. }
  12430. else { // edge connected to other node with the "from" side
  12431. edge.originalFromId.push(childNode.id);
  12432. edge.from = parentNode;
  12433. edge.fromId = parentNode.id;
  12434. }
  12435. this._addToReroutedEdges(parentNode,childNode,edge);
  12436. }
  12437. },
  12438. /**
  12439. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  12440. * these edges inside of the cluster.
  12441. *
  12442. * @param parentNode
  12443. * @param childNode
  12444. * @private
  12445. */
  12446. _containCircularEdgesFromNode : function(parentNode, childNode) {
  12447. // manage all the edges connected to the child and parent nodes
  12448. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12449. var edge = parentNode.dynamicEdges[i];
  12450. // handle circular edges
  12451. if (edge.toId == edge.fromId) {
  12452. this._addToContainedEdges(parentNode, childNode, edge);
  12453. }
  12454. }
  12455. },
  12456. /**
  12457. * This adds an edge from the childNode to the rerouted edges of the parent node
  12458. *
  12459. * @param parentNode | Node object
  12460. * @param childNode | Node object
  12461. * @param edge | Edge object
  12462. * @private
  12463. */
  12464. _addToReroutedEdges : function(parentNode, childNode, edge) {
  12465. // create an array object if it does not yet exist for this childNode
  12466. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  12467. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  12468. parentNode.reroutedEdges[childNode.id] = [];
  12469. }
  12470. parentNode.reroutedEdges[childNode.id].push(edge);
  12471. // this edge becomes part of the dynamicEdges of the cluster node
  12472. parentNode.dynamicEdges.push(edge);
  12473. },
  12474. /**
  12475. * This function connects an edge that was connected to a cluster node back to the child node.
  12476. *
  12477. * @param parentNode | Node object
  12478. * @param childNode | Node object
  12479. * @private
  12480. */
  12481. _connectEdgeBackToChild : function(parentNode, childNode) {
  12482. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  12483. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  12484. var edge = parentNode.reroutedEdges[childNode.id][i];
  12485. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  12486. edge.originalFromId.pop();
  12487. edge.fromId = childNode.id;
  12488. edge.from = childNode;
  12489. }
  12490. else {
  12491. edge.originalToId.pop();
  12492. edge.toId = childNode.id;
  12493. edge.to = childNode;
  12494. }
  12495. // append this edge to the list of edges connecting to the childnode
  12496. childNode.dynamicEdges.push(edge);
  12497. // remove the edge from the parent object
  12498. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  12499. if (parentNode.dynamicEdges[j].id == edge.id) {
  12500. parentNode.dynamicEdges.splice(j,1);
  12501. break;
  12502. }
  12503. }
  12504. }
  12505. // remove the entry from the rerouted edges
  12506. delete parentNode.reroutedEdges[childNode.id];
  12507. }
  12508. },
  12509. /**
  12510. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  12511. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  12512. * parentNode
  12513. *
  12514. * @param parentNode | Node object
  12515. * @private
  12516. */
  12517. _validateEdges : function(parentNode) {
  12518. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12519. var edge = parentNode.dynamicEdges[i];
  12520. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  12521. parentNode.dynamicEdges.splice(i,1);
  12522. }
  12523. }
  12524. },
  12525. /**
  12526. * This function released the contained edges back into the global domain and puts them back into the
  12527. * dynamic edges of both parent and child.
  12528. *
  12529. * @param {Node} parentNode |
  12530. * @param {Node} childNode |
  12531. * @private
  12532. */
  12533. _releaseContainedEdges : function(parentNode, childNode) {
  12534. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  12535. var edge = parentNode.containedEdges[childNode.id][i];
  12536. // put the edge back in the global edges object
  12537. this.edges[edge.id] = edge;
  12538. // put the edge back in the dynamic edges of the child and parent
  12539. childNode.dynamicEdges.push(edge);
  12540. parentNode.dynamicEdges.push(edge);
  12541. }
  12542. // remove the entry from the contained edges
  12543. delete parentNode.containedEdges[childNode.id];
  12544. },
  12545. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  12546. /**
  12547. * This updates the node labels for all nodes (for debugging purposes)
  12548. */
  12549. updateLabels : function() {
  12550. var nodeId;
  12551. // update node labels
  12552. for (nodeId in this.nodes) {
  12553. if (this.nodes.hasOwnProperty(nodeId)) {
  12554. var node = this.nodes[nodeId];
  12555. if (node.clusterSize > 1) {
  12556. node.label = "[".concat(String(node.clusterSize),"]");
  12557. }
  12558. }
  12559. }
  12560. // update node labels
  12561. for (nodeId in this.nodes) {
  12562. if (this.nodes.hasOwnProperty(nodeId)) {
  12563. node = this.nodes[nodeId];
  12564. if (node.clusterSize == 1) {
  12565. if (node.originalLabel !== undefined) {
  12566. node.label = node.originalLabel;
  12567. }
  12568. else {
  12569. node.label = String(node.id);
  12570. }
  12571. }
  12572. }
  12573. }
  12574. // /* Debug Override */
  12575. // for (nodeId in this.nodes) {
  12576. // if (this.nodes.hasOwnProperty(nodeId)) {
  12577. // node = this.nodes[nodeId];
  12578. // node.label = String(node.level);
  12579. // }
  12580. // }
  12581. },
  12582. /**
  12583. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  12584. * if the rest of the nodes are already a few cluster levels in.
  12585. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  12586. * clustered enough to the clusterToSmallestNeighbours function.
  12587. */
  12588. normalizeClusterLevels : function() {
  12589. var maxLevel = 0;
  12590. var minLevel = 1e9;
  12591. var clusterLevel = 0;
  12592. var nodeId;
  12593. // we loop over all nodes in the list
  12594. for (nodeId in this.nodes) {
  12595. if (this.nodes.hasOwnProperty(nodeId)) {
  12596. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  12597. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  12598. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  12599. }
  12600. }
  12601. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  12602. var amountOfNodes = this.nodeIndices.length;
  12603. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  12604. // we loop over all nodes in the list
  12605. for (nodeId in this.nodes) {
  12606. if (this.nodes.hasOwnProperty(nodeId)) {
  12607. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  12608. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  12609. }
  12610. }
  12611. }
  12612. this._updateNodeIndexList();
  12613. this._updateDynamicEdges();
  12614. // if a cluster was formed, we increase the clusterSession
  12615. if (this.nodeIndices.length != amountOfNodes) {
  12616. this.clusterSession += 1;
  12617. }
  12618. }
  12619. },
  12620. /**
  12621. * This function determines if the cluster we want to decluster is in the active area
  12622. * this means around the zoom center
  12623. *
  12624. * @param {Node} node
  12625. * @returns {boolean}
  12626. * @private
  12627. */
  12628. _nodeInActiveArea : function(node) {
  12629. return (
  12630. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12631. &&
  12632. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12633. )
  12634. },
  12635. /**
  12636. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  12637. * It puts large clusters away from the center and randomizes the order.
  12638. *
  12639. */
  12640. repositionNodes : function() {
  12641. for (var i = 0; i < this.nodeIndices.length; i++) {
  12642. var node = this.nodes[this.nodeIndices[i]];
  12643. if ((node.xFixed == false || node.yFixed == false)) {
  12644. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  12645. var angle = 2 * Math.PI * Math.random();
  12646. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  12647. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  12648. this._repositionBezierNodes(node);
  12649. }
  12650. }
  12651. },
  12652. /**
  12653. * We determine how many connections denote an important hub.
  12654. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  12655. *
  12656. * @private
  12657. */
  12658. _getHubSize : function() {
  12659. var average = 0;
  12660. var averageSquared = 0;
  12661. var hubCounter = 0;
  12662. var largestHub = 0;
  12663. for (var i = 0; i < this.nodeIndices.length; i++) {
  12664. var node = this.nodes[this.nodeIndices[i]];
  12665. if (node.dynamicEdgesLength > largestHub) {
  12666. largestHub = node.dynamicEdgesLength;
  12667. }
  12668. average += node.dynamicEdgesLength;
  12669. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  12670. hubCounter += 1;
  12671. }
  12672. average = average / hubCounter;
  12673. averageSquared = averageSquared / hubCounter;
  12674. var variance = averageSquared - Math.pow(average,2);
  12675. var standardDeviation = Math.sqrt(variance);
  12676. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  12677. // always have at least one to cluster
  12678. if (this.hubThreshold > largestHub) {
  12679. this.hubThreshold = largestHub;
  12680. }
  12681. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  12682. // console.log("hubThreshold:",this.hubThreshold);
  12683. },
  12684. /**
  12685. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12686. * with this amount we can cluster specifically on these chains.
  12687. *
  12688. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  12689. * @private
  12690. */
  12691. _reduceAmountOfChains : function(fraction) {
  12692. this.hubThreshold = 2;
  12693. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  12694. for (var nodeId in this.nodes) {
  12695. if (this.nodes.hasOwnProperty(nodeId)) {
  12696. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12697. if (reduceAmount > 0) {
  12698. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  12699. reduceAmount -= 1;
  12700. }
  12701. }
  12702. }
  12703. }
  12704. },
  12705. /**
  12706. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12707. * with this amount we can cluster specifically on these chains.
  12708. *
  12709. * @private
  12710. */
  12711. _getChainFraction : function() {
  12712. var chains = 0;
  12713. var total = 0;
  12714. for (var nodeId in this.nodes) {
  12715. if (this.nodes.hasOwnProperty(nodeId)) {
  12716. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12717. chains += 1;
  12718. }
  12719. total += 1;
  12720. }
  12721. }
  12722. return chains/total;
  12723. }
  12724. };
  12725. var SelectionMixin = {
  12726. /**
  12727. * This function can be called from the _doInAllSectors function
  12728. *
  12729. * @param object
  12730. * @param overlappingNodes
  12731. * @private
  12732. */
  12733. _getNodesOverlappingWith : function(object, overlappingNodes) {
  12734. var nodes = this.nodes;
  12735. for (var nodeId in nodes) {
  12736. if (nodes.hasOwnProperty(nodeId)) {
  12737. if (nodes[nodeId].isOverlappingWith(object)) {
  12738. overlappingNodes.push(nodeId);
  12739. }
  12740. }
  12741. }
  12742. },
  12743. /**
  12744. * retrieve all nodes overlapping with given object
  12745. * @param {Object} object An object with parameters left, top, right, bottom
  12746. * @return {Number[]} An array with id's of the overlapping nodes
  12747. * @private
  12748. */
  12749. _getAllNodesOverlappingWith : function (object) {
  12750. var overlappingNodes = [];
  12751. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  12752. return overlappingNodes;
  12753. },
  12754. /**
  12755. * Return a position object in canvasspace from a single point in screenspace
  12756. *
  12757. * @param pointer
  12758. * @returns {{left: number, top: number, right: number, bottom: number}}
  12759. * @private
  12760. */
  12761. _pointerToPositionObject : function(pointer) {
  12762. var x = this._canvasToX(pointer.x);
  12763. var y = this._canvasToY(pointer.y);
  12764. return {left: x,
  12765. top: y,
  12766. right: x,
  12767. bottom: y};
  12768. },
  12769. /**
  12770. * Get the top node at the a specific point (like a click)
  12771. *
  12772. * @param {{x: Number, y: Number}} pointer
  12773. * @return {Node | null} node
  12774. * @private
  12775. */
  12776. _getNodeAt : function (pointer) {
  12777. // we first check if this is an navigation controls element
  12778. var positionObject = this._pointerToPositionObject(pointer);
  12779. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  12780. // if there are overlapping nodes, select the last one, this is the
  12781. // one which is drawn on top of the others
  12782. if (overlappingNodes.length > 0) {
  12783. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  12784. }
  12785. else {
  12786. return null;
  12787. }
  12788. },
  12789. /**
  12790. * retrieve all edges overlapping with given object, selector is around center
  12791. * @param {Object} object An object with parameters left, top, right, bottom
  12792. * @return {Number[]} An array with id's of the overlapping nodes
  12793. * @private
  12794. */
  12795. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  12796. var edges = this.edges;
  12797. for (var edgeId in edges) {
  12798. if (edges.hasOwnProperty(edgeId)) {
  12799. if (edges[edgeId].isOverlappingWith(object)) {
  12800. overlappingEdges.push(edgeId);
  12801. }
  12802. }
  12803. }
  12804. },
  12805. /**
  12806. * retrieve all nodes overlapping with given object
  12807. * @param {Object} object An object with parameters left, top, right, bottom
  12808. * @return {Number[]} An array with id's of the overlapping nodes
  12809. * @private
  12810. */
  12811. _getAllEdgesOverlappingWith : function (object) {
  12812. var overlappingEdges = [];
  12813. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  12814. return overlappingEdges;
  12815. },
  12816. /**
  12817. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  12818. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  12819. *
  12820. * @param pointer
  12821. * @returns {null}
  12822. * @private
  12823. */
  12824. _getEdgeAt : function(pointer) {
  12825. var positionObject = this._pointerToPositionObject(pointer);
  12826. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  12827. if (overlappingEdges.length > 0) {
  12828. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  12829. }
  12830. else {
  12831. return null;
  12832. }
  12833. },
  12834. /**
  12835. * Add object to the selection array.
  12836. *
  12837. * @param obj
  12838. * @private
  12839. */
  12840. _addToSelection : function(obj) {
  12841. if (obj instanceof Node) {
  12842. this.selectionObj.nodes[obj.id] = obj;
  12843. }
  12844. else {
  12845. this.selectionObj.edges[obj.id] = obj;
  12846. }
  12847. },
  12848. /**
  12849. * Remove a single option from selection.
  12850. *
  12851. * @param {Object} obj
  12852. * @private
  12853. */
  12854. _removeFromSelection : function(obj) {
  12855. if (obj instanceof Node) {
  12856. delete this.selectionObj.nodes[obj.id];
  12857. }
  12858. else {
  12859. delete this.selectionObj.edges[obj.id];
  12860. }
  12861. },
  12862. /**
  12863. * Unselect all. The selectionObj is useful for this.
  12864. *
  12865. * @param {Boolean} [doNotTrigger] | ignore trigger
  12866. * @private
  12867. */
  12868. _unselectAll : function(doNotTrigger) {
  12869. if (doNotTrigger === undefined) {
  12870. doNotTrigger = false;
  12871. }
  12872. for(var nodeId in this.selectionObj.nodes) {
  12873. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12874. this.selectionObj.nodes[nodeId].unselect();
  12875. }
  12876. }
  12877. for(var edgeId in this.selectionObj.edges) {
  12878. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12879. this.selectionObj.edges[edgeId].unselect();
  12880. }
  12881. }
  12882. this.selectionObj = {nodes:{},edges:{}};
  12883. if (doNotTrigger == false) {
  12884. this.emit('select', this.getSelection());
  12885. }
  12886. },
  12887. /**
  12888. * Unselect all clusters. The selectionObj is useful for this.
  12889. *
  12890. * @param {Boolean} [doNotTrigger] | ignore trigger
  12891. * @private
  12892. */
  12893. _unselectClusters : function(doNotTrigger) {
  12894. if (doNotTrigger === undefined) {
  12895. doNotTrigger = false;
  12896. }
  12897. for (var nodeId in this.selectionObj.nodes) {
  12898. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12899. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  12900. this.selectionObj.nodes[nodeId].unselect();
  12901. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  12902. }
  12903. }
  12904. }
  12905. if (doNotTrigger == false) {
  12906. this.emit('select', this.getSelection());
  12907. }
  12908. },
  12909. /**
  12910. * return the number of selected nodes
  12911. *
  12912. * @returns {number}
  12913. * @private
  12914. */
  12915. _getSelectedNodeCount : function() {
  12916. var count = 0;
  12917. for (var nodeId in this.selectionObj.nodes) {
  12918. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12919. count += 1;
  12920. }
  12921. }
  12922. return count;
  12923. },
  12924. /**
  12925. * return the number of selected nodes
  12926. *
  12927. * @returns {number}
  12928. * @private
  12929. */
  12930. _getSelectedNode : function() {
  12931. for (var nodeId in this.selectionObj.nodes) {
  12932. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12933. return this.selectionObj.nodes[nodeId];
  12934. }
  12935. }
  12936. return null;
  12937. },
  12938. /**
  12939. * return the number of selected edges
  12940. *
  12941. * @returns {number}
  12942. * @private
  12943. */
  12944. _getSelectedEdgeCount : function() {
  12945. var count = 0;
  12946. for (var edgeId in this.selectionObj.edges) {
  12947. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12948. count += 1;
  12949. }
  12950. }
  12951. return count;
  12952. },
  12953. /**
  12954. * return the number of selected objects.
  12955. *
  12956. * @returns {number}
  12957. * @private
  12958. */
  12959. _getSelectedObjectCount : function() {
  12960. var count = 0;
  12961. for(var nodeId in this.selectionObj.nodes) {
  12962. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12963. count += 1;
  12964. }
  12965. }
  12966. for(var edgeId in this.selectionObj.edges) {
  12967. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12968. count += 1;
  12969. }
  12970. }
  12971. return count;
  12972. },
  12973. /**
  12974. * Check if anything is selected
  12975. *
  12976. * @returns {boolean}
  12977. * @private
  12978. */
  12979. _selectionIsEmpty : function() {
  12980. for(var nodeId in this.selectionObj.nodes) {
  12981. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  12982. return false;
  12983. }
  12984. }
  12985. for(var edgeId in this.selectionObj.edges) {
  12986. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  12987. return false;
  12988. }
  12989. }
  12990. return true;
  12991. },
  12992. /**
  12993. * check if one of the selected nodes is a cluster.
  12994. *
  12995. * @returns {boolean}
  12996. * @private
  12997. */
  12998. _clusterInSelection : function() {
  12999. for(var nodeId in this.selectionObj.nodes) {
  13000. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13001. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  13002. return true;
  13003. }
  13004. }
  13005. }
  13006. return false;
  13007. },
  13008. /**
  13009. * select the edges connected to the node that is being selected
  13010. *
  13011. * @param {Node} node
  13012. * @private
  13013. */
  13014. _selectConnectedEdges : function(node) {
  13015. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13016. var edge = node.dynamicEdges[i];
  13017. edge.select();
  13018. this._addToSelection(edge);
  13019. }
  13020. },
  13021. /**
  13022. * unselect the edges connected to the node that is being selected
  13023. *
  13024. * @param {Node} node
  13025. * @private
  13026. */
  13027. _unselectConnectedEdges : function(node) {
  13028. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13029. var edge = node.dynamicEdges[i];
  13030. edge.unselect();
  13031. this._removeFromSelection(edge);
  13032. }
  13033. },
  13034. /**
  13035. * This is called when someone clicks on a node. either select or deselect it.
  13036. * If there is an existing selection and we don't want to append to it, clear the existing selection
  13037. *
  13038. * @param {Node || Edge} object
  13039. * @param {Boolean} append
  13040. * @param {Boolean} [doNotTrigger] | ignore trigger
  13041. * @private
  13042. */
  13043. _selectObject : function(object, append, doNotTrigger) {
  13044. if (doNotTrigger === undefined) {
  13045. doNotTrigger = false;
  13046. }
  13047. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  13048. this._unselectAll(true);
  13049. }
  13050. if (object.selected == false) {
  13051. object.select();
  13052. this._addToSelection(object);
  13053. if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
  13054. this._selectConnectedEdges(object);
  13055. }
  13056. }
  13057. else {
  13058. object.unselect();
  13059. this._removeFromSelection(object);
  13060. }
  13061. if (doNotTrigger == false) {
  13062. this.emit('select', this.getSelection());
  13063. }
  13064. },
  13065. /**
  13066. * handles the selection part of the touch, only for navigation controls elements;
  13067. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  13068. * This is the most responsive solution
  13069. *
  13070. * @param {Object} pointer
  13071. * @private
  13072. */
  13073. _handleTouch : function(pointer) {
  13074. },
  13075. /**
  13076. * handles the selection part of the tap;
  13077. *
  13078. * @param {Object} pointer
  13079. * @private
  13080. */
  13081. _handleTap : function(pointer) {
  13082. var node = this._getNodeAt(pointer);
  13083. if (node != null) {
  13084. this._selectObject(node,false);
  13085. }
  13086. else {
  13087. var edge = this._getEdgeAt(pointer);
  13088. if (edge != null) {
  13089. this._selectObject(edge,false);
  13090. }
  13091. else {
  13092. this._unselectAll();
  13093. }
  13094. }
  13095. this.emit("click", this.getSelection());
  13096. this._redraw();
  13097. },
  13098. /**
  13099. * handles the selection part of the double tap and opens a cluster if needed
  13100. *
  13101. * @param {Object} pointer
  13102. * @private
  13103. */
  13104. _handleDoubleTap : function(pointer) {
  13105. var node = this._getNodeAt(pointer);
  13106. if (node != null && node !== undefined) {
  13107. // we reset the areaCenter here so the opening of the node will occur
  13108. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  13109. "y" : this._canvasToY(pointer.y)};
  13110. this.openCluster(node);
  13111. }
  13112. this.emit("doubleClick", this.getSelection());
  13113. },
  13114. /**
  13115. * Handle the onHold selection part
  13116. *
  13117. * @param pointer
  13118. * @private
  13119. */
  13120. _handleOnHold : function(pointer) {
  13121. var node = this._getNodeAt(pointer);
  13122. if (node != null) {
  13123. this._selectObject(node,true);
  13124. }
  13125. else {
  13126. var edge = this._getEdgeAt(pointer);
  13127. if (edge != null) {
  13128. this._selectObject(edge,true);
  13129. }
  13130. }
  13131. this._redraw();
  13132. },
  13133. /**
  13134. * handle the onRelease event. These functions are here for the navigation controls module.
  13135. *
  13136. * @private
  13137. */
  13138. _handleOnRelease : function(pointer) {
  13139. },
  13140. /**
  13141. *
  13142. * retrieve the currently selected objects
  13143. * @return {Number[] | String[]} selection An array with the ids of the
  13144. * selected nodes.
  13145. */
  13146. getSelection : function() {
  13147. var nodeIds = this.getSelectedNodes();
  13148. var edgeIds = this.getSelectedEdges();
  13149. return {nodes:nodeIds, edges:edgeIds};
  13150. },
  13151. /**
  13152. *
  13153. * retrieve the currently selected nodes
  13154. * @return {String} selection An array with the ids of the
  13155. * selected nodes.
  13156. */
  13157. getSelectedNodes : function() {
  13158. var idArray = [];
  13159. for(var nodeId in this.selectionObj.nodes) {
  13160. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13161. idArray.push(nodeId);
  13162. }
  13163. }
  13164. return idArray
  13165. },
  13166. /**
  13167. *
  13168. * retrieve the currently selected edges
  13169. * @return {Array} selection An array with the ids of the
  13170. * selected nodes.
  13171. */
  13172. getSelectedEdges : function() {
  13173. var idArray = [];
  13174. for(var edgeId in this.selectionObj.edges) {
  13175. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13176. idArray.push(edgeId);
  13177. }
  13178. }
  13179. return idArray;
  13180. },
  13181. /**
  13182. * select zero or more nodes
  13183. * @param {Number[] | String[]} selection An array with the ids of the
  13184. * selected nodes.
  13185. */
  13186. setSelection : function(selection) {
  13187. var i, iMax, id;
  13188. if (!selection || (selection.length == undefined))
  13189. throw 'Selection must be an array with ids';
  13190. // first unselect any selected node
  13191. this._unselectAll(true);
  13192. for (i = 0, iMax = selection.length; i < iMax; i++) {
  13193. id = selection[i];
  13194. var node = this.nodes[id];
  13195. if (!node) {
  13196. throw new RangeError('Node with id "' + id + '" not found');
  13197. }
  13198. this._selectObject(node,true,true);
  13199. }
  13200. this.redraw();
  13201. },
  13202. /**
  13203. * Validate the selection: remove ids of nodes which no longer exist
  13204. * @private
  13205. */
  13206. _updateSelection : function () {
  13207. for(var nodeId in this.selectionObj.nodes) {
  13208. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13209. if (!this.nodes.hasOwnProperty(nodeId)) {
  13210. delete this.selectionObj.nodes[nodeId];
  13211. }
  13212. }
  13213. }
  13214. for(var edgeId in this.selectionObj.edges) {
  13215. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13216. if (!this.edges.hasOwnProperty(edgeId)) {
  13217. delete this.selectionObj.edges[edgeId];
  13218. }
  13219. }
  13220. }
  13221. }
  13222. };
  13223. /**
  13224. * Created by Alex on 1/22/14.
  13225. */
  13226. var NavigationMixin = {
  13227. _cleanNavigation : function() {
  13228. // clean up previosu navigation items
  13229. var wrapper = document.getElementById('graph-navigation_wrapper');
  13230. if (wrapper != null) {
  13231. this.containerElement.removeChild(wrapper);
  13232. }
  13233. document.onmouseup = null;
  13234. },
  13235. /**
  13236. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  13237. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  13238. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  13239. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  13240. *
  13241. * @private
  13242. */
  13243. _loadNavigationElements : function() {
  13244. this._cleanNavigation();
  13245. this.navigationDivs = {};
  13246. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  13247. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  13248. this.navigationDivs['wrapper'] = document.createElement('div');
  13249. this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
  13250. this.navigationDivs['wrapper'].style.position = "absolute";
  13251. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  13252. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  13253. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  13254. for (var i = 0; i < navigationDivs.length; i++) {
  13255. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  13256. this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
  13257. this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
  13258. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  13259. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  13260. }
  13261. document.onmouseup = this._stopMovement.bind(this);
  13262. },
  13263. /**
  13264. * this stops all movement induced by the navigation buttons
  13265. *
  13266. * @private
  13267. */
  13268. _stopMovement : function() {
  13269. this._xStopMoving();
  13270. this._yStopMoving();
  13271. this._stopZoom();
  13272. },
  13273. /**
  13274. * stops the actions performed by page up and down etc.
  13275. *
  13276. * @param event
  13277. * @private
  13278. */
  13279. _preventDefault : function(event) {
  13280. if (event !== undefined) {
  13281. if (event.preventDefault) {
  13282. event.preventDefault();
  13283. } else {
  13284. event.returnValue = false;
  13285. }
  13286. }
  13287. },
  13288. /**
  13289. * move the screen up
  13290. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  13291. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  13292. * To avoid this behaviour, we do the translation in the start loop.
  13293. *
  13294. * @private
  13295. */
  13296. _moveUp : function(event) {
  13297. this.yIncrement = this.constants.keyboard.speed.y;
  13298. this.start(); // if there is no node movement, the calculation wont be done
  13299. this._preventDefault(event);
  13300. if (this.navigationDivs) {
  13301. this.navigationDivs['up'].className += " active";
  13302. }
  13303. },
  13304. /**
  13305. * move the screen down
  13306. * @private
  13307. */
  13308. _moveDown : function(event) {
  13309. this.yIncrement = -this.constants.keyboard.speed.y;
  13310. this.start(); // if there is no node movement, the calculation wont be done
  13311. this._preventDefault(event);
  13312. if (this.navigationDivs) {
  13313. this.navigationDivs['down'].className += " active";
  13314. }
  13315. },
  13316. /**
  13317. * move the screen left
  13318. * @private
  13319. */
  13320. _moveLeft : function(event) {
  13321. this.xIncrement = this.constants.keyboard.speed.x;
  13322. this.start(); // if there is no node movement, the calculation wont be done
  13323. this._preventDefault(event);
  13324. if (this.navigationDivs) {
  13325. this.navigationDivs['left'].className += " active";
  13326. }
  13327. },
  13328. /**
  13329. * move the screen right
  13330. * @private
  13331. */
  13332. _moveRight : function(event) {
  13333. this.xIncrement = -this.constants.keyboard.speed.y;
  13334. this.start(); // if there is no node movement, the calculation wont be done
  13335. this._preventDefault(event);
  13336. if (this.navigationDivs) {
  13337. this.navigationDivs['right'].className += " active";
  13338. }
  13339. },
  13340. /**
  13341. * Zoom in, using the same method as the movement.
  13342. * @private
  13343. */
  13344. _zoomIn : function(event) {
  13345. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  13346. this.start(); // if there is no node movement, the calculation wont be done
  13347. this._preventDefault(event);
  13348. if (this.navigationDivs) {
  13349. this.navigationDivs['zoomIn'].className += " active";
  13350. }
  13351. },
  13352. /**
  13353. * Zoom out
  13354. * @private
  13355. */
  13356. _zoomOut : function() {
  13357. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  13358. this.start(); // if there is no node movement, the calculation wont be done
  13359. this._preventDefault(event);
  13360. if (this.navigationDivs) {
  13361. this.navigationDivs['zoomOut'].className += " active";
  13362. }
  13363. },
  13364. /**
  13365. * Stop zooming and unhighlight the zoom controls
  13366. * @private
  13367. */
  13368. _stopZoom : function() {
  13369. this.zoomIncrement = 0;
  13370. if (this.navigationDivs) {
  13371. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  13372. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  13373. }
  13374. },
  13375. /**
  13376. * Stop moving in the Y direction and unHighlight the up and down
  13377. * @private
  13378. */
  13379. _yStopMoving : function() {
  13380. this.yIncrement = 0;
  13381. if (this.navigationDivs) {
  13382. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  13383. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  13384. }
  13385. },
  13386. /**
  13387. * Stop moving in the X direction and unHighlight left and right.
  13388. * @private
  13389. */
  13390. _xStopMoving : function() {
  13391. this.xIncrement = 0;
  13392. if (this.navigationDivs) {
  13393. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  13394. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  13395. }
  13396. }
  13397. };
  13398. /**
  13399. * Created by Alex on 2/10/14.
  13400. */
  13401. var graphMixinLoaders = {
  13402. /**
  13403. * Load a mixin into the graph object
  13404. *
  13405. * @param {Object} sourceVariable | this object has to contain functions.
  13406. * @private
  13407. */
  13408. _loadMixin: function (sourceVariable) {
  13409. for (var mixinFunction in sourceVariable) {
  13410. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13411. Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
  13412. }
  13413. }
  13414. },
  13415. /**
  13416. * removes a mixin from the graph object.
  13417. *
  13418. * @param {Object} sourceVariable | this object has to contain functions.
  13419. * @private
  13420. */
  13421. _clearMixin: function (sourceVariable) {
  13422. for (var mixinFunction in sourceVariable) {
  13423. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13424. Graph.prototype[mixinFunction] = undefined;
  13425. }
  13426. }
  13427. },
  13428. /**
  13429. * Mixin the physics system and initialize the parameters required.
  13430. *
  13431. * @private
  13432. */
  13433. _loadPhysicsSystem: function () {
  13434. this._loadMixin(physicsMixin);
  13435. this._loadSelectedForceSolver();
  13436. if (this.constants.configurePhysics == true) {
  13437. this._loadPhysicsConfiguration();
  13438. }
  13439. },
  13440. /**
  13441. * Mixin the cluster system and initialize the parameters required.
  13442. *
  13443. * @private
  13444. */
  13445. _loadClusterSystem: function () {
  13446. this.clusterSession = 0;
  13447. this.hubThreshold = 5;
  13448. this._loadMixin(ClusterMixin);
  13449. },
  13450. /**
  13451. * Mixin the sector system and initialize the parameters required
  13452. *
  13453. * @private
  13454. */
  13455. _loadSectorSystem: function () {
  13456. this.sectors = {};
  13457. this.activeSector = ["default"];
  13458. this.sectors["active"] = {};
  13459. this.sectors["active"]["default"] = {"nodes": {},
  13460. "edges": {},
  13461. "nodeIndices": [],
  13462. "formationScale": 1.0,
  13463. "drawingNode": undefined };
  13464. this.sectors["frozen"] = {};
  13465. this.sectors["support"] = {"nodes": {},
  13466. "edges": {},
  13467. "nodeIndices": [],
  13468. "formationScale": 1.0,
  13469. "drawingNode": undefined };
  13470. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  13471. this._loadMixin(SectorMixin);
  13472. },
  13473. /**
  13474. * Mixin the selection system and initialize the parameters required
  13475. *
  13476. * @private
  13477. */
  13478. _loadSelectionSystem: function () {
  13479. this.selectionObj = {nodes: {}, edges: {}};
  13480. this._loadMixin(SelectionMixin);
  13481. },
  13482. /**
  13483. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  13484. *
  13485. * @private
  13486. */
  13487. _loadManipulationSystem: function () {
  13488. // reset global variables -- these are used by the selection of nodes and edges.
  13489. this.blockConnectingEdgeSelection = false;
  13490. this.forceAppendSelection = false;
  13491. if (this.constants.dataManipulation.enabled == true) {
  13492. // load the manipulator HTML elements. All styling done in css.
  13493. if (this.manipulationDiv === undefined) {
  13494. this.manipulationDiv = document.createElement('div');
  13495. this.manipulationDiv.className = 'graph-manipulationDiv';
  13496. this.manipulationDiv.id = 'graph-manipulationDiv';
  13497. if (this.editMode == true) {
  13498. this.manipulationDiv.style.display = "block";
  13499. }
  13500. else {
  13501. this.manipulationDiv.style.display = "none";
  13502. }
  13503. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  13504. }
  13505. if (this.editModeDiv === undefined) {
  13506. this.editModeDiv = document.createElement('div');
  13507. this.editModeDiv.className = 'graph-manipulation-editMode';
  13508. this.editModeDiv.id = 'graph-manipulation-editMode';
  13509. if (this.editMode == true) {
  13510. this.editModeDiv.style.display = "none";
  13511. }
  13512. else {
  13513. this.editModeDiv.style.display = "block";
  13514. }
  13515. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  13516. }
  13517. if (this.closeDiv === undefined) {
  13518. this.closeDiv = document.createElement('div');
  13519. this.closeDiv.className = 'graph-manipulation-closeDiv';
  13520. this.closeDiv.id = 'graph-manipulation-closeDiv';
  13521. this.closeDiv.style.display = this.manipulationDiv.style.display;
  13522. this.containerElement.insertBefore(this.closeDiv, this.frame);
  13523. }
  13524. // load the manipulation functions
  13525. this._loadMixin(manipulationMixin);
  13526. // create the manipulator toolbar
  13527. this._createManipulatorBar();
  13528. }
  13529. else {
  13530. if (this.manipulationDiv !== undefined) {
  13531. // removes all the bindings and overloads
  13532. this._createManipulatorBar();
  13533. // remove the manipulation divs
  13534. this.containerElement.removeChild(this.manipulationDiv);
  13535. this.containerElement.removeChild(this.editModeDiv);
  13536. this.containerElement.removeChild(this.closeDiv);
  13537. this.manipulationDiv = undefined;
  13538. this.editModeDiv = undefined;
  13539. this.closeDiv = undefined;
  13540. // remove the mixin functions
  13541. this._clearMixin(manipulationMixin);
  13542. }
  13543. }
  13544. },
  13545. /**
  13546. * Mixin the navigation (User Interface) system and initialize the parameters required
  13547. *
  13548. * @private
  13549. */
  13550. _loadNavigationControls: function () {
  13551. this._loadMixin(NavigationMixin);
  13552. // the clean function removes the button divs, this is done to remove the bindings.
  13553. this._cleanNavigation();
  13554. if (this.constants.navigation.enabled == true) {
  13555. this._loadNavigationElements();
  13556. }
  13557. },
  13558. /**
  13559. * Mixin the hierarchical layout system.
  13560. *
  13561. * @private
  13562. */
  13563. _loadHierarchySystem: function () {
  13564. this._loadMixin(HierarchicalLayoutMixin);
  13565. }
  13566. };
  13567. /**
  13568. * @constructor Graph
  13569. * Create a graph visualization, displaying nodes and edges.
  13570. *
  13571. * @param {Element} container The DOM element in which the Graph will
  13572. * be created. Normally a div element.
  13573. * @param {Object} data An object containing parameters
  13574. * {Array} nodes
  13575. * {Array} edges
  13576. * @param {Object} options Options
  13577. */
  13578. function Graph (container, data, options) {
  13579. this._initializeMixinLoaders();
  13580. // create variables and set default values
  13581. this.containerElement = container;
  13582. this.width = '100%';
  13583. this.height = '100%';
  13584. // render and calculation settings
  13585. this.renderRefreshRate = 60; // hz (fps)
  13586. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  13587. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  13588. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  13589. this.physicsDiscreteStepsize = 0.65; // discrete stepsize of the simulation
  13590. this.stabilize = true; // stabilize before displaying the graph
  13591. this.selectable = true;
  13592. this.initializing = true;
  13593. // these functions are triggered when the dataset is edited
  13594. this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
  13595. // set constant values
  13596. this.constants = {
  13597. nodes: {
  13598. radiusMin: 5,
  13599. radiusMax: 20,
  13600. radius: 5,
  13601. shape: 'ellipse',
  13602. image: undefined,
  13603. widthMin: 16, // px
  13604. widthMax: 64, // px
  13605. fixed: false,
  13606. fontColor: 'black',
  13607. fontSize: 14, // px
  13608. fontFace: 'verdana',
  13609. level: -1,
  13610. color: {
  13611. border: '#2B7CE9',
  13612. background: '#97C2FC',
  13613. highlight: {
  13614. border: '#2B7CE9',
  13615. background: '#D2E5FF'
  13616. }
  13617. },
  13618. borderColor: '#2B7CE9',
  13619. backgroundColor: '#97C2FC',
  13620. highlightColor: '#D2E5FF',
  13621. group: undefined
  13622. },
  13623. edges: {
  13624. widthMin: 1,
  13625. widthMax: 15,
  13626. width: 1,
  13627. style: 'line',
  13628. color: {
  13629. color:'#848484',
  13630. highlight:'#848484'
  13631. },
  13632. fontColor: '#343434',
  13633. fontSize: 14, // px
  13634. fontFace: 'arial',
  13635. fontFill: 'white',
  13636. arrowScaleFactor: 1,
  13637. dash: {
  13638. length: 10,
  13639. gap: 5,
  13640. altLength: undefined
  13641. }
  13642. },
  13643. configurePhysics:false,
  13644. physics: {
  13645. barnesHut: {
  13646. enabled: true,
  13647. theta: 1 / 0.6, // inverted to save time during calculation
  13648. gravitationalConstant: -2000,
  13649. centralGravity: 0.3,
  13650. springLength: 95,
  13651. springConstant: 0.04,
  13652. damping: 0.09
  13653. },
  13654. repulsion: {
  13655. centralGravity: 0.1,
  13656. springLength: 200,
  13657. springConstant: 0.05,
  13658. nodeDistance: 100,
  13659. damping: 0.09
  13660. },
  13661. hierarchicalRepulsion: {
  13662. enabled: false,
  13663. centralGravity: 0.0,
  13664. springLength: 100,
  13665. springConstant: 0.01,
  13666. nodeDistance: 60,
  13667. damping: 0.09
  13668. },
  13669. damping: null,
  13670. centralGravity: null,
  13671. springLength: null,
  13672. springConstant: null
  13673. },
  13674. clustering: { // Per Node in Cluster = PNiC
  13675. enabled: false, // (Boolean) | global on/off switch for clustering.
  13676. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  13677. 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
  13678. 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
  13679. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  13680. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  13681. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  13682. 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.
  13683. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  13684. maxFontSize: 1000,
  13685. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  13686. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  13687. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  13688. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  13689. height: 1, // (px PNiC) | growth of the height per node in cluster.
  13690. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  13691. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  13692. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  13693. clusterLevelDifference: 2
  13694. },
  13695. navigation: {
  13696. enabled: false
  13697. },
  13698. keyboard: {
  13699. enabled: false,
  13700. speed: {x: 10, y: 10, zoom: 0.02}
  13701. },
  13702. dataManipulation: {
  13703. enabled: false,
  13704. initiallyVisible: false
  13705. },
  13706. hierarchicalLayout: {
  13707. enabled:false,
  13708. levelSeparation: 150,
  13709. nodeSpacing: 100,
  13710. direction: "UD" // UD, DU, LR, RL
  13711. },
  13712. freezeForStabilization: false,
  13713. smoothCurves: true,
  13714. maxVelocity: 10,
  13715. minVelocity: 0.1, // px/s
  13716. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  13717. labels:{
  13718. add:"Add Node",
  13719. edit:"Edit",
  13720. link:"Add Link",
  13721. del:"Delete selected",
  13722. editNode:"Edit Node",
  13723. back:"Back",
  13724. addDescription:"Click in an empty space to place a new node.",
  13725. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  13726. addError:"The function for add does not support two arguments (data,callback).",
  13727. linkError:"The function for connect does not support two arguments (data,callback).",
  13728. editError:"The function for edit does not support two arguments (data, callback).",
  13729. editBoundError:"No edit function has been bound to this button.",
  13730. deleteError:"The function for delete does not support two arguments (data, callback).",
  13731. deleteClusterError:"Clusters cannot be deleted."
  13732. },
  13733. tooltip: {
  13734. delay: 300,
  13735. fontColor: 'black',
  13736. fontSize: 14, // px
  13737. fontFace: 'verdana',
  13738. color: {
  13739. border: '#666',
  13740. background: '#FFFFC6'
  13741. }
  13742. }
  13743. };
  13744. this.editMode = this.constants.dataManipulation.initiallyVisible;
  13745. // Node variables
  13746. var graph = this;
  13747. this.groups = new Groups(); // object with groups
  13748. this.images = new Images(); // object with images
  13749. this.images.setOnloadCallback(function () {
  13750. graph._redraw();
  13751. });
  13752. // keyboard navigation variables
  13753. this.xIncrement = 0;
  13754. this.yIncrement = 0;
  13755. this.zoomIncrement = 0;
  13756. // loading all the mixins:
  13757. // load the force calculation functions, grouped under the physics system.
  13758. this._loadPhysicsSystem();
  13759. // create a frame and canvas
  13760. this._create();
  13761. // load the sector system. (mandatory, fully integrated with Graph)
  13762. this._loadSectorSystem();
  13763. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  13764. this._loadClusterSystem();
  13765. // load the selection system. (mandatory, required by Graph)
  13766. this._loadSelectionSystem();
  13767. // load the selection system. (mandatory, required by Graph)
  13768. this._loadHierarchySystem();
  13769. // apply options
  13770. this.setOptions(options);
  13771. // other vars
  13772. this.freezeSimulation = false;// freeze the simulation
  13773. this.cachedFunctions = {};
  13774. // containers for nodes and edges
  13775. this.calculationNodes = {};
  13776. this.calculationNodeIndices = [];
  13777. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  13778. this.nodes = {}; // object with Node objects
  13779. this.edges = {}; // object with Edge objects
  13780. // position and scale variables and objects
  13781. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  13782. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  13783. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  13784. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  13785. this.scale = 1; // defining the global scale variable in the constructor
  13786. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  13787. // datasets or dataviews
  13788. this.nodesData = null; // A DataSet or DataView
  13789. this.edgesData = null; // A DataSet or DataView
  13790. // create event listeners used to subscribe on the DataSets of the nodes and edges
  13791. this.nodesListeners = {
  13792. 'add': function (event, params) {
  13793. graph._addNodes(params.items);
  13794. graph.start();
  13795. },
  13796. 'update': function (event, params) {
  13797. graph._updateNodes(params.items);
  13798. graph.start();
  13799. },
  13800. 'remove': function (event, params) {
  13801. graph._removeNodes(params.items);
  13802. graph.start();
  13803. }
  13804. };
  13805. this.edgesListeners = {
  13806. 'add': function (event, params) {
  13807. graph._addEdges(params.items);
  13808. graph.start();
  13809. },
  13810. 'update': function (event, params) {
  13811. graph._updateEdges(params.items);
  13812. graph.start();
  13813. },
  13814. 'remove': function (event, params) {
  13815. graph._removeEdges(params.items);
  13816. graph.start();
  13817. }
  13818. };
  13819. // properties for the animation
  13820. this.moving = true;
  13821. this.timer = undefined; // Scheduling function. Is definded in this.start();
  13822. // load data (the disable start variable will be the same as the enabled clustering)
  13823. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  13824. // hierarchical layout
  13825. this.initializing = false;
  13826. if (this.constants.hierarchicalLayout.enabled == true) {
  13827. this._setupHierarchicalLayout();
  13828. }
  13829. else {
  13830. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  13831. if (this.stabilize == false) {
  13832. this.zoomExtent(true,this.constants.clustering.enabled);
  13833. }
  13834. }
  13835. // if clustering is disabled, the simulation will have started in the setData function
  13836. if (this.constants.clustering.enabled) {
  13837. this.startWithClustering();
  13838. }
  13839. }
  13840. // Extend Graph with an Emitter mixin
  13841. Emitter(Graph.prototype);
  13842. /**
  13843. * Get the script path where the vis.js library is located
  13844. *
  13845. * @returns {string | null} path Path or null when not found. Path does not
  13846. * end with a slash.
  13847. * @private
  13848. */
  13849. Graph.prototype._getScriptPath = function() {
  13850. var scripts = document.getElementsByTagName( 'script' );
  13851. // find script named vis.js or vis.min.js
  13852. for (var i = 0; i < scripts.length; i++) {
  13853. var src = scripts[i].src;
  13854. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  13855. if (match) {
  13856. // return path without the script name
  13857. return src.substring(0, src.length - match[0].length);
  13858. }
  13859. }
  13860. return null;
  13861. };
  13862. /**
  13863. * Find the center position of the graph
  13864. * @private
  13865. */
  13866. Graph.prototype._getRange = function() {
  13867. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  13868. for (var nodeId in this.nodes) {
  13869. if (this.nodes.hasOwnProperty(nodeId)) {
  13870. node = this.nodes[nodeId];
  13871. if (minX > (node.x)) {minX = node.x;}
  13872. if (maxX < (node.x)) {maxX = node.x;}
  13873. if (minY > (node.y)) {minY = node.y;}
  13874. if (maxY < (node.y)) {maxY = node.y;}
  13875. }
  13876. }
  13877. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  13878. minY = 0, maxY = 0, minX = 0, maxX = 0;
  13879. }
  13880. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13881. };
  13882. /**
  13883. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13884. * @returns {{x: number, y: number}}
  13885. * @private
  13886. */
  13887. Graph.prototype._findCenter = function(range) {
  13888. return {x: (0.5 * (range.maxX + range.minX)),
  13889. y: (0.5 * (range.maxY + range.minY))};
  13890. };
  13891. /**
  13892. * center the graph
  13893. *
  13894. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  13895. */
  13896. Graph.prototype._centerGraph = function(range) {
  13897. var center = this._findCenter(range);
  13898. center.x *= this.scale;
  13899. center.y *= this.scale;
  13900. center.x -= 0.5 * this.frame.canvas.clientWidth;
  13901. center.y -= 0.5 * this.frame.canvas.clientHeight;
  13902. this._setTranslation(-center.x,-center.y); // set at 0,0
  13903. };
  13904. /**
  13905. * This function zooms out to fit all data on screen based on amount of nodes
  13906. *
  13907. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  13908. * @param {Boolean} [disableStart] | If true, start is not called.
  13909. */
  13910. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  13911. if (initialZoom === undefined) {
  13912. initialZoom = false;
  13913. }
  13914. if (disableStart === undefined) {
  13915. disableStart = false;
  13916. }
  13917. var range = this._getRange();
  13918. var zoomLevel;
  13919. if (initialZoom == true) {
  13920. var numberOfNodes = this.nodeIndices.length;
  13921. if (this.constants.smoothCurves == true) {
  13922. if (this.constants.clustering.enabled == true &&
  13923. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  13924. 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.
  13925. }
  13926. else {
  13927. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  13928. }
  13929. }
  13930. else {
  13931. if (this.constants.clustering.enabled == true &&
  13932. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  13933. 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.
  13934. }
  13935. else {
  13936. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  13937. }
  13938. }
  13939. // correct for larger canvasses.
  13940. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  13941. zoomLevel *= factor;
  13942. }
  13943. else {
  13944. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  13945. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  13946. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  13947. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  13948. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  13949. }
  13950. if (zoomLevel > 1.0) {
  13951. zoomLevel = 1.0;
  13952. }
  13953. this._setScale(zoomLevel);
  13954. this._centerGraph(range);
  13955. if (disableStart == false) {
  13956. this.moving = true;
  13957. this.start();
  13958. }
  13959. };
  13960. /**
  13961. * Update the this.nodeIndices with the most recent node index list
  13962. * @private
  13963. */
  13964. Graph.prototype._updateNodeIndexList = function() {
  13965. this._clearNodeIndexList();
  13966. for (var idx in this.nodes) {
  13967. if (this.nodes.hasOwnProperty(idx)) {
  13968. this.nodeIndices.push(idx);
  13969. }
  13970. }
  13971. };
  13972. /**
  13973. * Set nodes and edges, and optionally options as well.
  13974. *
  13975. * @param {Object} data Object containing parameters:
  13976. * {Array | DataSet | DataView} [nodes] Array with nodes
  13977. * {Array | DataSet | DataView} [edges] Array with edges
  13978. * {String} [dot] String containing data in DOT format
  13979. * {Options} [options] Object with options
  13980. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  13981. */
  13982. Graph.prototype.setData = function(data, disableStart) {
  13983. if (disableStart === undefined) {
  13984. disableStart = false;
  13985. }
  13986. if (data && data.dot && (data.nodes || data.edges)) {
  13987. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  13988. ' parameter pair "nodes" and "edges", but not both.');
  13989. }
  13990. // set options
  13991. this.setOptions(data && data.options);
  13992. // set all data
  13993. if (data && data.dot) {
  13994. // parse DOT file
  13995. if(data && data.dot) {
  13996. var dotData = vis.util.DOTToGraph(data.dot);
  13997. this.setData(dotData);
  13998. return;
  13999. }
  14000. }
  14001. else {
  14002. this._setNodes(data && data.nodes);
  14003. this._setEdges(data && data.edges);
  14004. }
  14005. this._putDataInSector();
  14006. if (!disableStart) {
  14007. // find a stable position or start animating to a stable position
  14008. if (this.stabilize) {
  14009. this._stabilize();
  14010. }
  14011. this.start();
  14012. }
  14013. };
  14014. /**
  14015. * Set options
  14016. * @param {Object} options
  14017. */
  14018. Graph.prototype.setOptions = function (options) {
  14019. if (options) {
  14020. var prop;
  14021. // retrieve parameter values
  14022. if (options.width !== undefined) {this.width = options.width;}
  14023. if (options.height !== undefined) {this.height = options.height;}
  14024. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  14025. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  14026. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  14027. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  14028. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  14029. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  14030. if (options.labels !== undefined) {
  14031. for (prop in options.labels) {
  14032. if (options.labels.hasOwnProperty(prop)) {
  14033. this.constants.labels[prop] = options.labels[prop];
  14034. }
  14035. }
  14036. }
  14037. if (options.onAdd) {
  14038. this.triggerFunctions.add = options.onAdd;
  14039. }
  14040. if (options.onEdit) {
  14041. this.triggerFunctions.edit = options.onEdit;
  14042. }
  14043. if (options.onConnect) {
  14044. this.triggerFunctions.connect = options.onConnect;
  14045. }
  14046. if (options.onDelete) {
  14047. this.triggerFunctions.del = options.onDelete;
  14048. }
  14049. if (options.physics) {
  14050. if (options.physics.barnesHut) {
  14051. this.constants.physics.barnesHut.enabled = true;
  14052. for (prop in options.physics.barnesHut) {
  14053. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  14054. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  14055. }
  14056. }
  14057. }
  14058. if (options.physics.repulsion) {
  14059. this.constants.physics.barnesHut.enabled = false;
  14060. for (prop in options.physics.repulsion) {
  14061. if (options.physics.repulsion.hasOwnProperty(prop)) {
  14062. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  14063. }
  14064. }
  14065. }
  14066. if (options.physics.hierarchicalRepulsion) {
  14067. this.constants.hierarchicalLayout.enabled = true;
  14068. this.constants.physics.hierarchicalRepulsion.enabled = true;
  14069. this.constants.physics.barnesHut.enabled = false;
  14070. for (prop in options.physics.hierarchicalRepulsion) {
  14071. if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
  14072. this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
  14073. }
  14074. }
  14075. }
  14076. }
  14077. if (options.hierarchicalLayout) {
  14078. this.constants.hierarchicalLayout.enabled = true;
  14079. for (prop in options.hierarchicalLayout) {
  14080. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  14081. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  14082. }
  14083. }
  14084. }
  14085. else if (options.hierarchicalLayout !== undefined) {
  14086. this.constants.hierarchicalLayout.enabled = false;
  14087. }
  14088. if (options.clustering) {
  14089. this.constants.clustering.enabled = true;
  14090. for (prop in options.clustering) {
  14091. if (options.clustering.hasOwnProperty(prop)) {
  14092. this.constants.clustering[prop] = options.clustering[prop];
  14093. }
  14094. }
  14095. }
  14096. else if (options.clustering !== undefined) {
  14097. this.constants.clustering.enabled = false;
  14098. }
  14099. if (options.navigation) {
  14100. this.constants.navigation.enabled = true;
  14101. for (prop in options.navigation) {
  14102. if (options.navigation.hasOwnProperty(prop)) {
  14103. this.constants.navigation[prop] = options.navigation[prop];
  14104. }
  14105. }
  14106. }
  14107. else if (options.navigation !== undefined) {
  14108. this.constants.navigation.enabled = false;
  14109. }
  14110. if (options.keyboard) {
  14111. this.constants.keyboard.enabled = true;
  14112. for (prop in options.keyboard) {
  14113. if (options.keyboard.hasOwnProperty(prop)) {
  14114. this.constants.keyboard[prop] = options.keyboard[prop];
  14115. }
  14116. }
  14117. }
  14118. else if (options.keyboard !== undefined) {
  14119. this.constants.keyboard.enabled = false;
  14120. }
  14121. if (options.dataManipulation) {
  14122. this.constants.dataManipulation.enabled = true;
  14123. for (prop in options.dataManipulation) {
  14124. if (options.dataManipulation.hasOwnProperty(prop)) {
  14125. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  14126. }
  14127. }
  14128. }
  14129. else if (options.dataManipulation !== undefined) {
  14130. this.constants.dataManipulation.enabled = false;
  14131. }
  14132. // TODO: work out these options and document them
  14133. if (options.edges) {
  14134. for (prop in options.edges) {
  14135. if (options.edges.hasOwnProperty(prop)) {
  14136. if (typeof options.edges[prop] != "object") {
  14137. this.constants.edges[prop] = options.edges[prop];
  14138. }
  14139. }
  14140. }
  14141. if (options.edges.color !== undefined) {
  14142. if (util.isString(options.edges.color)) {
  14143. this.constants.edges.color = {};
  14144. this.constants.edges.color.color = options.edges.color;
  14145. this.constants.edges.color.highlight = options.edges.color;
  14146. }
  14147. else {
  14148. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  14149. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  14150. }
  14151. }
  14152. if (!options.edges.fontColor) {
  14153. if (options.edges.color !== undefined) {
  14154. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  14155. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  14156. }
  14157. }
  14158. // Added to support dashed lines
  14159. // David Jordan
  14160. // 2012-08-08
  14161. if (options.edges.dash) {
  14162. if (options.edges.dash.length !== undefined) {
  14163. this.constants.edges.dash.length = options.edges.dash.length;
  14164. }
  14165. if (options.edges.dash.gap !== undefined) {
  14166. this.constants.edges.dash.gap = options.edges.dash.gap;
  14167. }
  14168. if (options.edges.dash.altLength !== undefined) {
  14169. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  14170. }
  14171. }
  14172. }
  14173. if (options.nodes) {
  14174. for (prop in options.nodes) {
  14175. if (options.nodes.hasOwnProperty(prop)) {
  14176. this.constants.nodes[prop] = options.nodes[prop];
  14177. }
  14178. }
  14179. if (options.nodes.color) {
  14180. this.constants.nodes.color = util.parseColor(options.nodes.color);
  14181. }
  14182. /*
  14183. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  14184. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  14185. */
  14186. }
  14187. if (options.groups) {
  14188. for (var groupname in options.groups) {
  14189. if (options.groups.hasOwnProperty(groupname)) {
  14190. var group = options.groups[groupname];
  14191. this.groups.add(groupname, group);
  14192. }
  14193. }
  14194. }
  14195. if (options.tooltip) {
  14196. for (prop in options.tooltip) {
  14197. if (options.tooltip.hasOwnProperty(prop)) {
  14198. this.constants.tooltip[prop] = options.tooltip[prop];
  14199. }
  14200. }
  14201. if (options.tooltip.color) {
  14202. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  14203. }
  14204. }
  14205. }
  14206. // (Re)loading the mixins that can be enabled or disabled in the options.
  14207. // load the force calculation functions, grouped under the physics system.
  14208. this._loadPhysicsSystem();
  14209. // load the navigation system.
  14210. this._loadNavigationControls();
  14211. // load the data manipulation system
  14212. this._loadManipulationSystem();
  14213. // configure the smooth curves
  14214. this._configureSmoothCurves();
  14215. // bind keys. If disabled, this will not do anything;
  14216. this._createKeyBinds();
  14217. this.setSize(this.width, this.height);
  14218. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  14219. this._setScale(1);
  14220. this._redraw();
  14221. };
  14222. /**
  14223. * Create the main frame for the Graph.
  14224. * This function is executed once when a Graph object is created. The frame
  14225. * contains a canvas, and this canvas contains all objects like the axis and
  14226. * nodes.
  14227. * @private
  14228. */
  14229. Graph.prototype._create = function () {
  14230. // remove all elements from the container element.
  14231. while (this.containerElement.hasChildNodes()) {
  14232. this.containerElement.removeChild(this.containerElement.firstChild);
  14233. }
  14234. this.frame = document.createElement('div');
  14235. this.frame.className = 'graph-frame';
  14236. this.frame.style.position = 'relative';
  14237. this.frame.style.overflow = 'hidden';
  14238. // create the graph canvas (HTML canvas element)
  14239. this.frame.canvas = document.createElement( 'canvas' );
  14240. this.frame.canvas.style.position = 'relative';
  14241. this.frame.appendChild(this.frame.canvas);
  14242. if (!this.frame.canvas.getContext) {
  14243. var noCanvas = document.createElement( 'DIV' );
  14244. noCanvas.style.color = 'red';
  14245. noCanvas.style.fontWeight = 'bold' ;
  14246. noCanvas.style.padding = '10px';
  14247. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14248. this.frame.canvas.appendChild(noCanvas);
  14249. }
  14250. var me = this;
  14251. this.drag = {};
  14252. this.pinch = {};
  14253. this.hammer = Hammer(this.frame.canvas, {
  14254. prevent_default: true
  14255. });
  14256. this.hammer.on('tap', me._onTap.bind(me) );
  14257. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  14258. this.hammer.on('hold', me._onHold.bind(me) );
  14259. this.hammer.on('pinch', me._onPinch.bind(me) );
  14260. this.hammer.on('touch', me._onTouch.bind(me) );
  14261. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14262. this.hammer.on('drag', me._onDrag.bind(me) );
  14263. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14264. this.hammer.on('release', me._onRelease.bind(me) );
  14265. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14266. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14267. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14268. // add the frame to the container element
  14269. this.containerElement.appendChild(this.frame);
  14270. };
  14271. /**
  14272. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  14273. * @private
  14274. */
  14275. Graph.prototype._createKeyBinds = function() {
  14276. var me = this;
  14277. this.mousetrap = mousetrap;
  14278. this.mousetrap.reset();
  14279. if (this.constants.keyboard.enabled == true) {
  14280. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  14281. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  14282. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  14283. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  14284. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  14285. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  14286. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  14287. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  14288. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  14289. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  14290. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  14291. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  14292. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  14293. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  14294. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  14295. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  14296. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  14297. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  14298. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  14299. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  14300. }
  14301. if (this.constants.dataManipulation.enabled == true) {
  14302. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  14303. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  14304. }
  14305. };
  14306. /**
  14307. * Get the pointer location from a touch location
  14308. * @param {{pageX: Number, pageY: Number}} touch
  14309. * @return {{x: Number, y: Number}} pointer
  14310. * @private
  14311. */
  14312. Graph.prototype._getPointer = function (touch) {
  14313. return {
  14314. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14315. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14316. };
  14317. };
  14318. /**
  14319. * On start of a touch gesture, store the pointer
  14320. * @param event
  14321. * @private
  14322. */
  14323. Graph.prototype._onTouch = function (event) {
  14324. this.drag.pointer = this._getPointer(event.gesture.center);
  14325. this.drag.pinched = false;
  14326. this.pinch.scale = this._getScale();
  14327. this._handleTouch(this.drag.pointer);
  14328. };
  14329. /**
  14330. * handle drag start event
  14331. * @private
  14332. */
  14333. Graph.prototype._onDragStart = function () {
  14334. this._handleDragStart();
  14335. };
  14336. /**
  14337. * This function is called by _onDragStart.
  14338. * It is separated out because we can then overload it for the datamanipulation system.
  14339. *
  14340. * @private
  14341. */
  14342. Graph.prototype._handleDragStart = function() {
  14343. var drag = this.drag;
  14344. var node = this._getNodeAt(drag.pointer);
  14345. // note: drag.pointer is set in _onTouch to get the initial touch location
  14346. drag.dragging = true;
  14347. drag.selection = [];
  14348. drag.translation = this._getTranslation();
  14349. drag.nodeId = null;
  14350. if (node != null) {
  14351. drag.nodeId = node.id;
  14352. // select the clicked node if not yet selected
  14353. if (!node.isSelected()) {
  14354. this._selectObject(node,false);
  14355. }
  14356. // create an array with the selected nodes and their original location and status
  14357. for (var objectId in this.selectionObj.nodes) {
  14358. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  14359. var object = this.selectionObj.nodes[objectId];
  14360. var s = {
  14361. id: object.id,
  14362. node: object,
  14363. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14364. x: object.x,
  14365. y: object.y,
  14366. xFixed: object.xFixed,
  14367. yFixed: object.yFixed
  14368. };
  14369. object.xFixed = true;
  14370. object.yFixed = true;
  14371. drag.selection.push(s);
  14372. }
  14373. }
  14374. }
  14375. };
  14376. /**
  14377. * handle drag event
  14378. * @private
  14379. */
  14380. Graph.prototype._onDrag = function (event) {
  14381. this._handleOnDrag(event)
  14382. };
  14383. /**
  14384. * This function is called by _onDrag.
  14385. * It is separated out because we can then overload it for the datamanipulation system.
  14386. *
  14387. * @private
  14388. */
  14389. Graph.prototype._handleOnDrag = function(event) {
  14390. if (this.drag.pinched) {
  14391. return;
  14392. }
  14393. var pointer = this._getPointer(event.gesture.center);
  14394. var me = this,
  14395. drag = this.drag,
  14396. selection = drag.selection;
  14397. if (selection && selection.length) {
  14398. // calculate delta's and new location
  14399. var deltaX = pointer.x - drag.pointer.x,
  14400. deltaY = pointer.y - drag.pointer.y;
  14401. // update position of all selected nodes
  14402. selection.forEach(function (s) {
  14403. var node = s.node;
  14404. if (!s.xFixed) {
  14405. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  14406. }
  14407. if (!s.yFixed) {
  14408. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  14409. }
  14410. });
  14411. // start _animationStep if not yet running
  14412. if (!this.moving) {
  14413. this.moving = true;
  14414. this.start();
  14415. }
  14416. }
  14417. else {
  14418. // move the graph
  14419. var diffX = pointer.x - this.drag.pointer.x;
  14420. var diffY = pointer.y - this.drag.pointer.y;
  14421. this._setTranslation(
  14422. this.drag.translation.x + diffX,
  14423. this.drag.translation.y + diffY);
  14424. this._redraw();
  14425. this.moving = true;
  14426. this.start();
  14427. }
  14428. };
  14429. /**
  14430. * handle drag start event
  14431. * @private
  14432. */
  14433. Graph.prototype._onDragEnd = function () {
  14434. this.drag.dragging = false;
  14435. var selection = this.drag.selection;
  14436. if (selection) {
  14437. selection.forEach(function (s) {
  14438. // restore original xFixed and yFixed
  14439. s.node.xFixed = s.xFixed;
  14440. s.node.yFixed = s.yFixed;
  14441. });
  14442. }
  14443. };
  14444. /**
  14445. * handle tap/click event: select/unselect a node
  14446. * @private
  14447. */
  14448. Graph.prototype._onTap = function (event) {
  14449. var pointer = this._getPointer(event.gesture.center);
  14450. this.pointerPosition = pointer;
  14451. this._handleTap(pointer);
  14452. };
  14453. /**
  14454. * handle doubletap event
  14455. * @private
  14456. */
  14457. Graph.prototype._onDoubleTap = function (event) {
  14458. var pointer = this._getPointer(event.gesture.center);
  14459. this._handleDoubleTap(pointer);
  14460. };
  14461. /**
  14462. * handle long tap event: multi select nodes
  14463. * @private
  14464. */
  14465. Graph.prototype._onHold = function (event) {
  14466. var pointer = this._getPointer(event.gesture.center);
  14467. this.pointerPosition = pointer;
  14468. this._handleOnHold(pointer);
  14469. };
  14470. /**
  14471. * handle the release of the screen
  14472. *
  14473. * @private
  14474. */
  14475. Graph.prototype._onRelease = function (event) {
  14476. var pointer = this._getPointer(event.gesture.center);
  14477. this._handleOnRelease(pointer);
  14478. };
  14479. /**
  14480. * Handle pinch event
  14481. * @param event
  14482. * @private
  14483. */
  14484. Graph.prototype._onPinch = function (event) {
  14485. var pointer = this._getPointer(event.gesture.center);
  14486. this.drag.pinched = true;
  14487. if (!('scale' in this.pinch)) {
  14488. this.pinch.scale = 1;
  14489. }
  14490. // TODO: enabled moving while pinching?
  14491. var scale = this.pinch.scale * event.gesture.scale;
  14492. this._zoom(scale, pointer)
  14493. };
  14494. /**
  14495. * Zoom the graph in or out
  14496. * @param {Number} scale a number around 1, and between 0.01 and 10
  14497. * @param {{x: Number, y: Number}} pointer Position on screen
  14498. * @return {Number} appliedScale scale is limited within the boundaries
  14499. * @private
  14500. */
  14501. Graph.prototype._zoom = function(scale, pointer) {
  14502. var scaleOld = this._getScale();
  14503. if (scale < 0.00001) {
  14504. scale = 0.00001;
  14505. }
  14506. if (scale > 10) {
  14507. scale = 10;
  14508. }
  14509. // + this.frame.canvas.clientHeight / 2
  14510. var translation = this._getTranslation();
  14511. var scaleFrac = scale / scaleOld;
  14512. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14513. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14514. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  14515. "y" : this._canvasToY(pointer.y)};
  14516. this._setScale(scale);
  14517. this._setTranslation(tx, ty);
  14518. this.updateClustersDefault();
  14519. this._redraw();
  14520. if (scaleOld < scale) {
  14521. this.emit("zoom", {direction:"+"});
  14522. }
  14523. else {
  14524. this.emit("zoom", {direction:"-"});
  14525. }
  14526. return scale;
  14527. };
  14528. /**
  14529. * Event handler for mouse wheel event, used to zoom the timeline
  14530. * See http://adomas.org/javascript-mouse-wheel/
  14531. * https://github.com/EightMedia/hammer.js/issues/256
  14532. * @param {MouseEvent} event
  14533. * @private
  14534. */
  14535. Graph.prototype._onMouseWheel = function(event) {
  14536. // retrieve delta
  14537. var delta = 0;
  14538. if (event.wheelDelta) { /* IE/Opera. */
  14539. delta = event.wheelDelta/120;
  14540. } else if (event.detail) { /* Mozilla case. */
  14541. // In Mozilla, sign of delta is different than in IE.
  14542. // Also, delta is multiple of 3.
  14543. delta = -event.detail/3;
  14544. }
  14545. // If delta is nonzero, handle it.
  14546. // Basically, delta is now positive if wheel was scrolled up,
  14547. // and negative, if wheel was scrolled down.
  14548. if (delta) {
  14549. // calculate the new scale
  14550. var scale = this._getScale();
  14551. var zoom = delta / 10;
  14552. if (delta < 0) {
  14553. zoom = zoom / (1 - zoom);
  14554. }
  14555. scale *= (1 + zoom);
  14556. // calculate the pointer location
  14557. var gesture = util.fakeGesture(this, event);
  14558. var pointer = this._getPointer(gesture.center);
  14559. // apply the new scale
  14560. this._zoom(scale, pointer);
  14561. }
  14562. // Prevent default actions caused by mouse wheel.
  14563. event.preventDefault();
  14564. };
  14565. /**
  14566. * Mouse move handler for checking whether the title moves over a node with a title.
  14567. * @param {Event} event
  14568. * @private
  14569. */
  14570. Graph.prototype._onMouseMoveTitle = function (event) {
  14571. var gesture = util.fakeGesture(this, event);
  14572. var pointer = this._getPointer(gesture.center);
  14573. // check if the previously selected node is still selected
  14574. if (this.popupNode) {
  14575. this._checkHidePopup(pointer);
  14576. }
  14577. // start a timeout that will check if the mouse is positioned above
  14578. // an element
  14579. var me = this;
  14580. var checkShow = function() {
  14581. me._checkShowPopup(pointer);
  14582. };
  14583. if (this.popupTimer) {
  14584. clearInterval(this.popupTimer); // stop any running calculationTimer
  14585. }
  14586. if (!this.drag.dragging) {
  14587. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  14588. }
  14589. };
  14590. /**
  14591. * Check if there is an element on the given position in the graph
  14592. * (a node or edge). If so, and if this element has a title,
  14593. * show a popup window with its title.
  14594. *
  14595. * @param {{x:Number, y:Number}} pointer
  14596. * @private
  14597. */
  14598. Graph.prototype._checkShowPopup = function (pointer) {
  14599. var obj = {
  14600. left: this._canvasToX(pointer.x),
  14601. top: this._canvasToY(pointer.y),
  14602. right: this._canvasToX(pointer.x),
  14603. bottom: this._canvasToY(pointer.y)
  14604. };
  14605. var id;
  14606. var lastPopupNode = this.popupNode;
  14607. if (this.popupNode == undefined) {
  14608. // search the nodes for overlap, select the top one in case of multiple nodes
  14609. var nodes = this.nodes;
  14610. for (id in nodes) {
  14611. if (nodes.hasOwnProperty(id)) {
  14612. var node = nodes[id];
  14613. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  14614. this.popupNode = node;
  14615. break;
  14616. }
  14617. }
  14618. }
  14619. }
  14620. if (this.popupNode === undefined) {
  14621. // search the edges for overlap
  14622. var edges = this.edges;
  14623. for (id in edges) {
  14624. if (edges.hasOwnProperty(id)) {
  14625. var edge = edges[id];
  14626. if (edge.connected && (edge.getTitle() !== undefined) &&
  14627. edge.isOverlappingWith(obj)) {
  14628. this.popupNode = edge;
  14629. break;
  14630. }
  14631. }
  14632. }
  14633. }
  14634. if (this.popupNode) {
  14635. // show popup message window
  14636. if (this.popupNode != lastPopupNode) {
  14637. var me = this;
  14638. if (!me.popup) {
  14639. me.popup = new Popup(me.frame, me.constants.tooltip);
  14640. }
  14641. // adjust a small offset such that the mouse cursor is located in the
  14642. // bottom left location of the popup, and you can easily move over the
  14643. // popup area
  14644. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  14645. me.popup.setText(me.popupNode.getTitle());
  14646. me.popup.show();
  14647. }
  14648. }
  14649. else {
  14650. if (this.popup) {
  14651. this.popup.hide();
  14652. }
  14653. }
  14654. };
  14655. /**
  14656. * Check if the popup must be hided, which is the case when the mouse is no
  14657. * longer hovering on the object
  14658. * @param {{x:Number, y:Number}} pointer
  14659. * @private
  14660. */
  14661. Graph.prototype._checkHidePopup = function (pointer) {
  14662. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  14663. this.popupNode = undefined;
  14664. if (this.popup) {
  14665. this.popup.hide();
  14666. }
  14667. }
  14668. };
  14669. /**
  14670. * Set a new size for the graph
  14671. * @param {string} width Width in pixels or percentage (for example '800px'
  14672. * or '50%')
  14673. * @param {string} height Height in pixels or percentage (for example '400px'
  14674. * or '30%')
  14675. */
  14676. Graph.prototype.setSize = function(width, height) {
  14677. this.frame.style.width = width;
  14678. this.frame.style.height = height;
  14679. this.frame.canvas.style.width = '100%';
  14680. this.frame.canvas.style.height = '100%';
  14681. this.frame.canvas.width = this.frame.canvas.clientWidth;
  14682. this.frame.canvas.height = this.frame.canvas.clientHeight;
  14683. if (this.manipulationDiv !== undefined) {
  14684. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  14685. }
  14686. if (this.navigationDivs !== undefined) {
  14687. if (this.navigationDivs['wrapper'] !== undefined) {
  14688. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  14689. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  14690. }
  14691. }
  14692. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  14693. };
  14694. /**
  14695. * Set a data set with nodes for the graph
  14696. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  14697. * @private
  14698. */
  14699. Graph.prototype._setNodes = function(nodes) {
  14700. var oldNodesData = this.nodesData;
  14701. if (nodes instanceof DataSet || nodes instanceof DataView) {
  14702. this.nodesData = nodes;
  14703. }
  14704. else if (nodes instanceof Array) {
  14705. this.nodesData = new DataSet();
  14706. this.nodesData.add(nodes);
  14707. }
  14708. else if (!nodes) {
  14709. this.nodesData = new DataSet();
  14710. }
  14711. else {
  14712. throw new TypeError('Array or DataSet expected');
  14713. }
  14714. if (oldNodesData) {
  14715. // unsubscribe from old dataset
  14716. util.forEach(this.nodesListeners, function (callback, event) {
  14717. oldNodesData.off(event, callback);
  14718. });
  14719. }
  14720. // remove drawn nodes
  14721. this.nodes = {};
  14722. if (this.nodesData) {
  14723. // subscribe to new dataset
  14724. var me = this;
  14725. util.forEach(this.nodesListeners, function (callback, event) {
  14726. me.nodesData.on(event, callback);
  14727. });
  14728. // draw all new nodes
  14729. var ids = this.nodesData.getIds();
  14730. this._addNodes(ids);
  14731. }
  14732. this._updateSelection();
  14733. };
  14734. /**
  14735. * Add nodes
  14736. * @param {Number[] | String[]} ids
  14737. * @private
  14738. */
  14739. Graph.prototype._addNodes = function(ids) {
  14740. var id;
  14741. for (var i = 0, len = ids.length; i < len; i++) {
  14742. id = ids[i];
  14743. var data = this.nodesData.get(id);
  14744. var node = new Node(data, this.images, this.groups, this.constants);
  14745. this.nodes[id] = node; // note: this may replace an existing node
  14746. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  14747. var radius = 10 * 0.1*ids.length;
  14748. var angle = 2 * Math.PI * Math.random();
  14749. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  14750. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  14751. }
  14752. this.moving = true;
  14753. }
  14754. this._updateNodeIndexList();
  14755. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14756. this._resetLevels();
  14757. this._setupHierarchicalLayout();
  14758. }
  14759. this._updateCalculationNodes();
  14760. this._reconnectEdges();
  14761. this._updateValueRange(this.nodes);
  14762. this.updateLabels();
  14763. };
  14764. /**
  14765. * Update existing nodes, or create them when not yet existing
  14766. * @param {Number[] | String[]} ids
  14767. * @private
  14768. */
  14769. Graph.prototype._updateNodes = function(ids) {
  14770. var nodes = this.nodes,
  14771. nodesData = this.nodesData;
  14772. for (var i = 0, len = ids.length; i < len; i++) {
  14773. var id = ids[i];
  14774. var node = nodes[id];
  14775. var data = nodesData.get(id);
  14776. if (node) {
  14777. // update node
  14778. node.setProperties(data, this.constants);
  14779. }
  14780. else {
  14781. // create node
  14782. node = new Node(properties, this.images, this.groups, this.constants);
  14783. nodes[id] = node;
  14784. }
  14785. }
  14786. this.moving = true;
  14787. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14788. this._resetLevels();
  14789. this._setupHierarchicalLayout();
  14790. }
  14791. this._updateNodeIndexList();
  14792. this._reconnectEdges();
  14793. this._updateValueRange(nodes);
  14794. };
  14795. /**
  14796. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  14797. * @param {Number[] | String[]} ids
  14798. * @private
  14799. */
  14800. Graph.prototype._removeNodes = function(ids) {
  14801. var nodes = this.nodes;
  14802. for (var i = 0, len = ids.length; i < len; i++) {
  14803. var id = ids[i];
  14804. delete nodes[id];
  14805. }
  14806. this._updateNodeIndexList();
  14807. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14808. this._resetLevels();
  14809. this._setupHierarchicalLayout();
  14810. }
  14811. this._updateCalculationNodes();
  14812. this._reconnectEdges();
  14813. this._updateSelection();
  14814. this._updateValueRange(nodes);
  14815. };
  14816. /**
  14817. * Load edges by reading the data table
  14818. * @param {Array | DataSet | DataView} edges The data containing the edges.
  14819. * @private
  14820. * @private
  14821. */
  14822. Graph.prototype._setEdges = function(edges) {
  14823. var oldEdgesData = this.edgesData;
  14824. if (edges instanceof DataSet || edges instanceof DataView) {
  14825. this.edgesData = edges;
  14826. }
  14827. else if (edges instanceof Array) {
  14828. this.edgesData = new DataSet();
  14829. this.edgesData.add(edges);
  14830. }
  14831. else if (!edges) {
  14832. this.edgesData = new DataSet();
  14833. }
  14834. else {
  14835. throw new TypeError('Array or DataSet expected');
  14836. }
  14837. if (oldEdgesData) {
  14838. // unsubscribe from old dataset
  14839. util.forEach(this.edgesListeners, function (callback, event) {
  14840. oldEdgesData.off(event, callback);
  14841. });
  14842. }
  14843. // remove drawn edges
  14844. this.edges = {};
  14845. if (this.edgesData) {
  14846. // subscribe to new dataset
  14847. var me = this;
  14848. util.forEach(this.edgesListeners, function (callback, event) {
  14849. me.edgesData.on(event, callback);
  14850. });
  14851. // draw all new nodes
  14852. var ids = this.edgesData.getIds();
  14853. this._addEdges(ids);
  14854. }
  14855. this._reconnectEdges();
  14856. };
  14857. /**
  14858. * Add edges
  14859. * @param {Number[] | String[]} ids
  14860. * @private
  14861. */
  14862. Graph.prototype._addEdges = function (ids) {
  14863. var edges = this.edges,
  14864. edgesData = this.edgesData;
  14865. for (var i = 0, len = ids.length; i < len; i++) {
  14866. var id = ids[i];
  14867. var oldEdge = edges[id];
  14868. if (oldEdge) {
  14869. oldEdge.disconnect();
  14870. }
  14871. var data = edgesData.get(id, {"showInternalIds" : true});
  14872. edges[id] = new Edge(data, this, this.constants);
  14873. }
  14874. this.moving = true;
  14875. this._updateValueRange(edges);
  14876. this._createBezierNodes();
  14877. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14878. this._resetLevels();
  14879. this._setupHierarchicalLayout();
  14880. }
  14881. this._updateCalculationNodes();
  14882. };
  14883. /**
  14884. * Update existing edges, or create them when not yet existing
  14885. * @param {Number[] | String[]} ids
  14886. * @private
  14887. */
  14888. Graph.prototype._updateEdges = function (ids) {
  14889. var edges = this.edges,
  14890. edgesData = this.edgesData;
  14891. for (var i = 0, len = ids.length; i < len; i++) {
  14892. var id = ids[i];
  14893. var data = edgesData.get(id);
  14894. var edge = edges[id];
  14895. if (edge) {
  14896. // update edge
  14897. edge.disconnect();
  14898. edge.setProperties(data, this.constants);
  14899. edge.connect();
  14900. }
  14901. else {
  14902. // create edge
  14903. edge = new Edge(data, this, this.constants);
  14904. this.edges[id] = edge;
  14905. }
  14906. }
  14907. this._createBezierNodes();
  14908. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14909. this._resetLevels();
  14910. this._setupHierarchicalLayout();
  14911. }
  14912. this.moving = true;
  14913. this._updateValueRange(edges);
  14914. };
  14915. /**
  14916. * Remove existing edges. Non existing ids will be ignored
  14917. * @param {Number[] | String[]} ids
  14918. * @private
  14919. */
  14920. Graph.prototype._removeEdges = function (ids) {
  14921. var edges = this.edges;
  14922. for (var i = 0, len = ids.length; i < len; i++) {
  14923. var id = ids[i];
  14924. var edge = edges[id];
  14925. if (edge) {
  14926. if (edge.via != null) {
  14927. delete this.sectors['support']['nodes'][edge.via.id];
  14928. }
  14929. edge.disconnect();
  14930. delete edges[id];
  14931. }
  14932. }
  14933. this.moving = true;
  14934. this._updateValueRange(edges);
  14935. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  14936. this._resetLevels();
  14937. this._setupHierarchicalLayout();
  14938. }
  14939. this._updateCalculationNodes();
  14940. };
  14941. /**
  14942. * Reconnect all edges
  14943. * @private
  14944. */
  14945. Graph.prototype._reconnectEdges = function() {
  14946. var id,
  14947. nodes = this.nodes,
  14948. edges = this.edges;
  14949. for (id in nodes) {
  14950. if (nodes.hasOwnProperty(id)) {
  14951. nodes[id].edges = [];
  14952. }
  14953. }
  14954. for (id in edges) {
  14955. if (edges.hasOwnProperty(id)) {
  14956. var edge = edges[id];
  14957. edge.from = null;
  14958. edge.to = null;
  14959. edge.connect();
  14960. }
  14961. }
  14962. };
  14963. /**
  14964. * Update the values of all object in the given array according to the current
  14965. * value range of the objects in the array.
  14966. * @param {Object} obj An object containing a set of Edges or Nodes
  14967. * The objects must have a method getValue() and
  14968. * setValueRange(min, max).
  14969. * @private
  14970. */
  14971. Graph.prototype._updateValueRange = function(obj) {
  14972. var id;
  14973. // determine the range of the objects
  14974. var valueMin = undefined;
  14975. var valueMax = undefined;
  14976. for (id in obj) {
  14977. if (obj.hasOwnProperty(id)) {
  14978. var value = obj[id].getValue();
  14979. if (value !== undefined) {
  14980. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  14981. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  14982. }
  14983. }
  14984. }
  14985. // adjust the range of all objects
  14986. if (valueMin !== undefined && valueMax !== undefined) {
  14987. for (id in obj) {
  14988. if (obj.hasOwnProperty(id)) {
  14989. obj[id].setValueRange(valueMin, valueMax);
  14990. }
  14991. }
  14992. }
  14993. };
  14994. /**
  14995. * Redraw the graph with the current data
  14996. * chart will be resized too.
  14997. */
  14998. Graph.prototype.redraw = function() {
  14999. this.setSize(this.width, this.height);
  15000. this._redraw();
  15001. };
  15002. /**
  15003. * Redraw the graph with the current data
  15004. * @private
  15005. */
  15006. Graph.prototype._redraw = function() {
  15007. var ctx = this.frame.canvas.getContext('2d');
  15008. // clear the canvas
  15009. var w = this.frame.canvas.width;
  15010. var h = this.frame.canvas.height;
  15011. ctx.clearRect(0, 0, w, h);
  15012. // set scaling and translation
  15013. ctx.save();
  15014. ctx.translate(this.translation.x, this.translation.y);
  15015. ctx.scale(this.scale, this.scale);
  15016. this.canvasTopLeft = {
  15017. "x": this._canvasToX(0),
  15018. "y": this._canvasToY(0)
  15019. };
  15020. this.canvasBottomRight = {
  15021. "x": this._canvasToX(this.frame.canvas.clientWidth),
  15022. "y": this._canvasToY(this.frame.canvas.clientHeight)
  15023. };
  15024. this._doInAllSectors("_drawAllSectorNodes",ctx);
  15025. this._doInAllSectors("_drawEdges",ctx);
  15026. this._doInAllSectors("_drawNodes",ctx,false);
  15027. // this._doInSupportSector("_drawNodes",ctx,true);
  15028. // this._drawTree(ctx,"#F00F0F");
  15029. // restore original scaling and translation
  15030. ctx.restore();
  15031. };
  15032. /**
  15033. * Set the translation of the graph
  15034. * @param {Number} offsetX Horizontal offset
  15035. * @param {Number} offsetY Vertical offset
  15036. * @private
  15037. */
  15038. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  15039. if (this.translation === undefined) {
  15040. this.translation = {
  15041. x: 0,
  15042. y: 0
  15043. };
  15044. }
  15045. if (offsetX !== undefined) {
  15046. this.translation.x = offsetX;
  15047. }
  15048. if (offsetY !== undefined) {
  15049. this.translation.y = offsetY;
  15050. }
  15051. this.emit('viewChanged');
  15052. };
  15053. /**
  15054. * Get the translation of the graph
  15055. * @return {Object} translation An object with parameters x and y, both a number
  15056. * @private
  15057. */
  15058. Graph.prototype._getTranslation = function() {
  15059. return {
  15060. x: this.translation.x,
  15061. y: this.translation.y
  15062. };
  15063. };
  15064. /**
  15065. * Scale the graph
  15066. * @param {Number} scale Scaling factor 1.0 is unscaled
  15067. * @private
  15068. */
  15069. Graph.prototype._setScale = function(scale) {
  15070. this.scale = scale;
  15071. };
  15072. /**
  15073. * Get the current scale of the graph
  15074. * @return {Number} scale Scaling factor 1.0 is unscaled
  15075. * @private
  15076. */
  15077. Graph.prototype._getScale = function() {
  15078. return this.scale;
  15079. };
  15080. /**
  15081. * Convert a horizontal point on the HTML canvas to the x-value of the model
  15082. * @param {number} x
  15083. * @returns {number}
  15084. * @private
  15085. */
  15086. Graph.prototype._canvasToX = function(x) {
  15087. return (x - this.translation.x) / this.scale;
  15088. };
  15089. /**
  15090. * Convert an x-value in the model to a horizontal point on the HTML canvas
  15091. * @param {number} x
  15092. * @returns {number}
  15093. * @private
  15094. */
  15095. Graph.prototype._xToCanvas = function(x) {
  15096. return x * this.scale + this.translation.x;
  15097. };
  15098. /**
  15099. * Convert a vertical point on the HTML canvas to the y-value of the model
  15100. * @param {number} y
  15101. * @returns {number}
  15102. * @private
  15103. */
  15104. Graph.prototype._canvasToY = function(y) {
  15105. return (y - this.translation.y) / this.scale;
  15106. };
  15107. /**
  15108. * Convert an y-value in the model to a vertical point on the HTML canvas
  15109. * @param {number} y
  15110. * @returns {number}
  15111. * @private
  15112. */
  15113. Graph.prototype._yToCanvas = function(y) {
  15114. return y * this.scale + this.translation.y ;
  15115. };
  15116. /**
  15117. *
  15118. * @param {object} pos = {x: number, y: number}
  15119. * @returns {{x: number, y: number}}
  15120. * @constructor
  15121. */
  15122. Graph.prototype.DOMtoCanvas = function(pos) {
  15123. return {x:this._xToCanvas(pos.x),y:this._yToCanvas(pos.y)};
  15124. }
  15125. /**
  15126. *
  15127. * @param {object} pos = {x: number, y: number}
  15128. * @returns {{x: number, y: number}}
  15129. * @constructor
  15130. */
  15131. Graph.prototype.canvasToDOM = function(pos) {
  15132. return {x:this._canvasToX(pos.x),y:this._canvasToY(pos.y)};
  15133. }
  15134. /**
  15135. * Redraw all nodes
  15136. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15137. * @param {CanvasRenderingContext2D} ctx
  15138. * @param {Boolean} [alwaysShow]
  15139. * @private
  15140. */
  15141. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  15142. if (alwaysShow === undefined) {
  15143. alwaysShow = false;
  15144. }
  15145. // first draw the unselected nodes
  15146. var nodes = this.nodes;
  15147. var selected = [];
  15148. for (var id in nodes) {
  15149. if (nodes.hasOwnProperty(id)) {
  15150. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  15151. if (nodes[id].isSelected()) {
  15152. selected.push(id);
  15153. }
  15154. else {
  15155. if (nodes[id].inArea() || alwaysShow) {
  15156. nodes[id].draw(ctx);
  15157. }
  15158. }
  15159. }
  15160. }
  15161. // draw the selected nodes on top
  15162. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  15163. if (nodes[selected[s]].inArea() || alwaysShow) {
  15164. nodes[selected[s]].draw(ctx);
  15165. }
  15166. }
  15167. };
  15168. /**
  15169. * Redraw all edges
  15170. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15171. * @param {CanvasRenderingContext2D} ctx
  15172. * @private
  15173. */
  15174. Graph.prototype._drawEdges = function(ctx) {
  15175. var edges = this.edges;
  15176. for (var id in edges) {
  15177. if (edges.hasOwnProperty(id)) {
  15178. var edge = edges[id];
  15179. edge.setScale(this.scale);
  15180. if (edge.connected) {
  15181. edges[id].draw(ctx);
  15182. }
  15183. }
  15184. }
  15185. };
  15186. /**
  15187. * Find a stable position for all nodes
  15188. * @private
  15189. */
  15190. Graph.prototype._stabilize = function() {
  15191. if (this.constants.freezeForStabilization == true) {
  15192. this._freezeDefinedNodes();
  15193. }
  15194. // find stable position
  15195. var count = 0;
  15196. while (this.moving && count < this.constants.stabilizationIterations) {
  15197. this._physicsTick();
  15198. count++;
  15199. }
  15200. this.zoomExtent(false,true);
  15201. if (this.constants.freezeForStabilization == true) {
  15202. this._restoreFrozenNodes();
  15203. }
  15204. this.emit("stabilized",{iterations:count});
  15205. };
  15206. /**
  15207. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  15208. * because only the supportnodes for the smoothCurves have to settle.
  15209. *
  15210. * @private
  15211. */
  15212. Graph.prototype._freezeDefinedNodes = function() {
  15213. var nodes = this.nodes;
  15214. for (var id in nodes) {
  15215. if (nodes.hasOwnProperty(id)) {
  15216. if (nodes[id].x != null && nodes[id].y != null) {
  15217. nodes[id].fixedData.x = nodes[id].xFixed;
  15218. nodes[id].fixedData.y = nodes[id].yFixed;
  15219. nodes[id].xFixed = true;
  15220. nodes[id].yFixed = true;
  15221. }
  15222. }
  15223. }
  15224. };
  15225. /**
  15226. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  15227. *
  15228. * @private
  15229. */
  15230. Graph.prototype._restoreFrozenNodes = function() {
  15231. var nodes = this.nodes;
  15232. for (var id in nodes) {
  15233. if (nodes.hasOwnProperty(id)) {
  15234. if (nodes[id].fixedData.x != null) {
  15235. nodes[id].xFixed = nodes[id].fixedData.x;
  15236. nodes[id].yFixed = nodes[id].fixedData.y;
  15237. }
  15238. }
  15239. }
  15240. };
  15241. /**
  15242. * Check if any of the nodes is still moving
  15243. * @param {number} vmin the minimum velocity considered as 'moving'
  15244. * @return {boolean} true if moving, false if non of the nodes is moving
  15245. * @private
  15246. */
  15247. Graph.prototype._isMoving = function(vmin) {
  15248. var nodes = this.nodes;
  15249. for (var id in nodes) {
  15250. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15251. return true;
  15252. }
  15253. }
  15254. return false;
  15255. };
  15256. /**
  15257. * /**
  15258. * Perform one discrete step for all nodes
  15259. *
  15260. * @private
  15261. */
  15262. Graph.prototype._discreteStepNodes = function() {
  15263. var interval = this.physicsDiscreteStepsize;
  15264. var nodes = this.nodes;
  15265. var nodeId;
  15266. var nodesPresent = false;
  15267. if (this.constants.maxVelocity > 0) {
  15268. for (nodeId in nodes) {
  15269. if (nodes.hasOwnProperty(nodeId)) {
  15270. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  15271. nodesPresent = true;
  15272. }
  15273. }
  15274. }
  15275. else {
  15276. for (nodeId in nodes) {
  15277. if (nodes.hasOwnProperty(nodeId)) {
  15278. nodes[nodeId].discreteStep(interval);
  15279. nodesPresent = true;
  15280. }
  15281. }
  15282. }
  15283. if (nodesPresent == true) {
  15284. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  15285. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  15286. this.moving = true;
  15287. }
  15288. else {
  15289. this.moving = this._isMoving(vminCorrected);
  15290. }
  15291. }
  15292. };
  15293. /**
  15294. * A single simulation step (or "tick") in the physics simulation
  15295. *
  15296. * @private
  15297. */
  15298. Graph.prototype._physicsTick = function() {
  15299. if (!this.freezeSimulation) {
  15300. if (this.moving) {
  15301. this._doInAllActiveSectors("_initializeForceCalculation");
  15302. this._doInAllActiveSectors("_discreteStepNodes");
  15303. if (this.constants.smoothCurves) {
  15304. this._doInSupportSector("_discreteStepNodes");
  15305. }
  15306. this._findCenter(this._getRange())
  15307. }
  15308. }
  15309. };
  15310. /**
  15311. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  15312. * It reschedules itself at the beginning of the function
  15313. *
  15314. * @private
  15315. */
  15316. Graph.prototype._animationStep = function() {
  15317. // reset the timer so a new scheduled animation step can be set
  15318. this.timer = undefined;
  15319. // handle the keyboad movement
  15320. this._handleNavigation();
  15321. // this schedules a new animation step
  15322. this.start();
  15323. // start the physics simulation
  15324. var calculationTime = Date.now();
  15325. var maxSteps = 1;
  15326. this._physicsTick();
  15327. var timeRequired = Date.now() - calculationTime;
  15328. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  15329. this._physicsTick();
  15330. timeRequired = Date.now() - calculationTime;
  15331. maxSteps++;
  15332. }
  15333. // start the rendering process
  15334. var renderTime = Date.now();
  15335. this._redraw();
  15336. this.renderTime = Date.now() - renderTime;
  15337. };
  15338. if (typeof window !== 'undefined') {
  15339. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  15340. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  15341. }
  15342. /**
  15343. * Schedule a animation step with the refreshrate interval.
  15344. */
  15345. Graph.prototype.start = function() {
  15346. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  15347. if (!this.timer) {
  15348. var ua = navigator.userAgent.toLowerCase();
  15349. var requiresTimeout = false;
  15350. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  15351. requiresTimeout = true;
  15352. }
  15353. else if (ua.indexOf('safari') != -1) { // safari
  15354. if (ua.indexOf('chrome') <= -1) {
  15355. requiresTimeout = true;
  15356. }
  15357. }
  15358. if (requiresTimeout == true) {
  15359. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15360. }
  15361. else{
  15362. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15363. }
  15364. }
  15365. }
  15366. else {
  15367. this._redraw();
  15368. }
  15369. };
  15370. /**
  15371. * Move the graph according to the keyboard presses.
  15372. *
  15373. * @private
  15374. */
  15375. Graph.prototype._handleNavigation = function() {
  15376. if (this.xIncrement != 0 || this.yIncrement != 0) {
  15377. var translation = this._getTranslation();
  15378. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  15379. }
  15380. if (this.zoomIncrement != 0) {
  15381. var center = {
  15382. x: this.frame.canvas.clientWidth / 2,
  15383. y: this.frame.canvas.clientHeight / 2
  15384. };
  15385. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  15386. }
  15387. };
  15388. /**
  15389. * Freeze the _animationStep
  15390. */
  15391. Graph.prototype.toggleFreeze = function() {
  15392. if (this.freezeSimulation == false) {
  15393. this.freezeSimulation = true;
  15394. }
  15395. else {
  15396. this.freezeSimulation = false;
  15397. this.start();
  15398. }
  15399. };
  15400. /**
  15401. * This function cleans the support nodes if they are not needed and adds them when they are.
  15402. *
  15403. * @param {boolean} [disableStart]
  15404. * @private
  15405. */
  15406. Graph.prototype._configureSmoothCurves = function(disableStart) {
  15407. if (disableStart === undefined) {
  15408. disableStart = true;
  15409. }
  15410. if (this.constants.smoothCurves == true) {
  15411. this._createBezierNodes();
  15412. }
  15413. else {
  15414. // delete the support nodes
  15415. this.sectors['support']['nodes'] = {};
  15416. for (var edgeId in this.edges) {
  15417. if (this.edges.hasOwnProperty(edgeId)) {
  15418. this.edges[edgeId].smooth = false;
  15419. this.edges[edgeId].via = null;
  15420. }
  15421. }
  15422. }
  15423. this._updateCalculationNodes();
  15424. if (!disableStart) {
  15425. this.moving = true;
  15426. this.start();
  15427. }
  15428. };
  15429. /**
  15430. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  15431. * are used for the force calculation.
  15432. *
  15433. * @private
  15434. */
  15435. Graph.prototype._createBezierNodes = function() {
  15436. if (this.constants.smoothCurves == true) {
  15437. for (var edgeId in this.edges) {
  15438. if (this.edges.hasOwnProperty(edgeId)) {
  15439. var edge = this.edges[edgeId];
  15440. if (edge.via == null) {
  15441. edge.smooth = true;
  15442. var nodeId = "edgeId:".concat(edge.id);
  15443. this.sectors['support']['nodes'][nodeId] = new Node(
  15444. {id:nodeId,
  15445. mass:1,
  15446. shape:'circle',
  15447. image:"",
  15448. internalMultiplier:1
  15449. },{},{},this.constants);
  15450. edge.via = this.sectors['support']['nodes'][nodeId];
  15451. edge.via.parentEdgeId = edge.id;
  15452. edge.positionBezierNode();
  15453. }
  15454. }
  15455. }
  15456. }
  15457. };
  15458. /**
  15459. * load the functions that load the mixins into the prototype.
  15460. *
  15461. * @private
  15462. */
  15463. Graph.prototype._initializeMixinLoaders = function () {
  15464. for (var mixinFunction in graphMixinLoaders) {
  15465. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  15466. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  15467. }
  15468. }
  15469. };
  15470. /**
  15471. * Load the XY positions of the nodes into the dataset.
  15472. */
  15473. Graph.prototype.storePosition = function() {
  15474. var dataArray = [];
  15475. for (var nodeId in this.nodes) {
  15476. if (this.nodes.hasOwnProperty(nodeId)) {
  15477. var node = this.nodes[nodeId];
  15478. var allowedToMoveX = !this.nodes.xFixed;
  15479. var allowedToMoveY = !this.nodes.yFixed;
  15480. if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
  15481. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  15482. }
  15483. }
  15484. }
  15485. this.nodesData.update(dataArray);
  15486. };
  15487. /**
  15488. * vis.js module exports
  15489. */
  15490. var vis = {
  15491. util: util,
  15492. DataSet: DataSet,
  15493. DataView: DataView,
  15494. Range: Range,
  15495. stack: stack,
  15496. TimeStep: TimeStep,
  15497. components: {
  15498. items: {
  15499. Item: Item,
  15500. ItemBox: ItemBox,
  15501. ItemPoint: ItemPoint,
  15502. ItemRange: ItemRange
  15503. },
  15504. Component: Component,
  15505. Panel: Panel,
  15506. RootPanel: RootPanel,
  15507. ItemSet: ItemSet,
  15508. TimeAxis: TimeAxis
  15509. },
  15510. graph: {
  15511. Node: Node,
  15512. Edge: Edge,
  15513. Popup: Popup,
  15514. Groups: Groups,
  15515. Images: Images
  15516. },
  15517. Timeline: Timeline,
  15518. Graph: Graph
  15519. };
  15520. /**
  15521. * CommonJS module exports
  15522. */
  15523. if (typeof exports !== 'undefined') {
  15524. exports = vis;
  15525. }
  15526. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  15527. module.exports = vis;
  15528. }
  15529. /**
  15530. * AMD module exports
  15531. */
  15532. if (typeof(define) === 'function') {
  15533. define(function () {
  15534. return vis;
  15535. });
  15536. }
  15537. /**
  15538. * Window exports
  15539. */
  15540. if (typeof window !== 'undefined') {
  15541. // attach the module to the window, load as a regular javascript file
  15542. window['vis'] = vis;
  15543. }
  15544. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  15545. /**
  15546. * Expose `Emitter`.
  15547. */
  15548. module.exports = Emitter;
  15549. /**
  15550. * Initialize a new `Emitter`.
  15551. *
  15552. * @api public
  15553. */
  15554. function Emitter(obj) {
  15555. if (obj) return mixin(obj);
  15556. };
  15557. /**
  15558. * Mixin the emitter properties.
  15559. *
  15560. * @param {Object} obj
  15561. * @return {Object}
  15562. * @api private
  15563. */
  15564. function mixin(obj) {
  15565. for (var key in Emitter.prototype) {
  15566. obj[key] = Emitter.prototype[key];
  15567. }
  15568. return obj;
  15569. }
  15570. /**
  15571. * Listen on the given `event` with `fn`.
  15572. *
  15573. * @param {String} event
  15574. * @param {Function} fn
  15575. * @return {Emitter}
  15576. * @api public
  15577. */
  15578. Emitter.prototype.on =
  15579. Emitter.prototype.addEventListener = function(event, fn){
  15580. this._callbacks = this._callbacks || {};
  15581. (this._callbacks[event] = this._callbacks[event] || [])
  15582. .push(fn);
  15583. return this;
  15584. };
  15585. /**
  15586. * Adds an `event` listener that will be invoked a single
  15587. * time then automatically removed.
  15588. *
  15589. * @param {String} event
  15590. * @param {Function} fn
  15591. * @return {Emitter}
  15592. * @api public
  15593. */
  15594. Emitter.prototype.once = function(event, fn){
  15595. var self = this;
  15596. this._callbacks = this._callbacks || {};
  15597. function on() {
  15598. self.off(event, on);
  15599. fn.apply(this, arguments);
  15600. }
  15601. on.fn = fn;
  15602. this.on(event, on);
  15603. return this;
  15604. };
  15605. /**
  15606. * Remove the given callback for `event` or all
  15607. * registered callbacks.
  15608. *
  15609. * @param {String} event
  15610. * @param {Function} fn
  15611. * @return {Emitter}
  15612. * @api public
  15613. */
  15614. Emitter.prototype.off =
  15615. Emitter.prototype.removeListener =
  15616. Emitter.prototype.removeAllListeners =
  15617. Emitter.prototype.removeEventListener = function(event, fn){
  15618. this._callbacks = this._callbacks || {};
  15619. // all
  15620. if (0 == arguments.length) {
  15621. this._callbacks = {};
  15622. return this;
  15623. }
  15624. // specific event
  15625. var callbacks = this._callbacks[event];
  15626. if (!callbacks) return this;
  15627. // remove all handlers
  15628. if (1 == arguments.length) {
  15629. delete this._callbacks[event];
  15630. return this;
  15631. }
  15632. // remove specific handler
  15633. var cb;
  15634. for (var i = 0; i < callbacks.length; i++) {
  15635. cb = callbacks[i];
  15636. if (cb === fn || cb.fn === fn) {
  15637. callbacks.splice(i, 1);
  15638. break;
  15639. }
  15640. }
  15641. return this;
  15642. };
  15643. /**
  15644. * Emit `event` with the given args.
  15645. *
  15646. * @param {String} event
  15647. * @param {Mixed} ...
  15648. * @return {Emitter}
  15649. */
  15650. Emitter.prototype.emit = function(event){
  15651. this._callbacks = this._callbacks || {};
  15652. var args = [].slice.call(arguments, 1)
  15653. , callbacks = this._callbacks[event];
  15654. if (callbacks) {
  15655. callbacks = callbacks.slice(0);
  15656. for (var i = 0, len = callbacks.length; i < len; ++i) {
  15657. callbacks[i].apply(this, args);
  15658. }
  15659. }
  15660. return this;
  15661. };
  15662. /**
  15663. * Return array of callbacks for `event`.
  15664. *
  15665. * @param {String} event
  15666. * @return {Array}
  15667. * @api public
  15668. */
  15669. Emitter.prototype.listeners = function(event){
  15670. this._callbacks = this._callbacks || {};
  15671. return this._callbacks[event] || [];
  15672. };
  15673. /**
  15674. * Check if this emitter has `event` handlers.
  15675. *
  15676. * @param {String} event
  15677. * @return {Boolean}
  15678. * @api public
  15679. */
  15680. Emitter.prototype.hasListeners = function(event){
  15681. return !! this.listeners(event).length;
  15682. };
  15683. },{}],3:[function(require,module,exports){
  15684. /*! Hammer.JS - v1.0.5 - 2013-04-07
  15685. * http://eightmedia.github.com/hammer.js
  15686. *
  15687. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  15688. * Licensed under the MIT license */
  15689. (function(window, undefined) {
  15690. 'use strict';
  15691. /**
  15692. * Hammer
  15693. * use this to create instances
  15694. * @param {HTMLElement} element
  15695. * @param {Object} options
  15696. * @returns {Hammer.Instance}
  15697. * @constructor
  15698. */
  15699. var Hammer = function(element, options) {
  15700. return new Hammer.Instance(element, options || {});
  15701. };
  15702. // default settings
  15703. Hammer.defaults = {
  15704. // add styles and attributes to the element to prevent the browser from doing
  15705. // its native behavior. this doesnt prevent the scrolling, but cancels
  15706. // the contextmenu, tap highlighting etc
  15707. // set to false to disable this
  15708. stop_browser_behavior: {
  15709. // this also triggers onselectstart=false for IE
  15710. userSelect: 'none',
  15711. // this makes the element blocking in IE10 >, you could experiment with the value
  15712. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  15713. touchAction: 'none',
  15714. touchCallout: 'none',
  15715. contentZooming: 'none',
  15716. userDrag: 'none',
  15717. tapHighlightColor: 'rgba(0,0,0,0)'
  15718. }
  15719. // more settings are defined per gesture at gestures.js
  15720. };
  15721. // detect touchevents
  15722. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  15723. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  15724. // dont use mouseevents on mobile devices
  15725. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  15726. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  15727. // eventtypes per touchevent (start, move, end)
  15728. // are filled by Hammer.event.determineEventTypes on setup
  15729. Hammer.EVENT_TYPES = {};
  15730. // direction defines
  15731. Hammer.DIRECTION_DOWN = 'down';
  15732. Hammer.DIRECTION_LEFT = 'left';
  15733. Hammer.DIRECTION_UP = 'up';
  15734. Hammer.DIRECTION_RIGHT = 'right';
  15735. // pointer type
  15736. Hammer.POINTER_MOUSE = 'mouse';
  15737. Hammer.POINTER_TOUCH = 'touch';
  15738. Hammer.POINTER_PEN = 'pen';
  15739. // touch event defines
  15740. Hammer.EVENT_START = 'start';
  15741. Hammer.EVENT_MOVE = 'move';
  15742. Hammer.EVENT_END = 'end';
  15743. // hammer document where the base events are added at
  15744. Hammer.DOCUMENT = document;
  15745. // plugins namespace
  15746. Hammer.plugins = {};
  15747. // if the window events are set...
  15748. Hammer.READY = false;
  15749. /**
  15750. * setup events to detect gestures on the document
  15751. */
  15752. function setup() {
  15753. if(Hammer.READY) {
  15754. return;
  15755. }
  15756. // find what eventtypes we add listeners to
  15757. Hammer.event.determineEventTypes();
  15758. // Register all gestures inside Hammer.gestures
  15759. for(var name in Hammer.gestures) {
  15760. if(Hammer.gestures.hasOwnProperty(name)) {
  15761. Hammer.detection.register(Hammer.gestures[name]);
  15762. }
  15763. }
  15764. // Add touch events on the document
  15765. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  15766. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  15767. // Hammer is ready...!
  15768. Hammer.READY = true;
  15769. }
  15770. /**
  15771. * create new hammer instance
  15772. * all methods should return the instance itself, so it is chainable.
  15773. * @param {HTMLElement} element
  15774. * @param {Object} [options={}]
  15775. * @returns {Hammer.Instance}
  15776. * @constructor
  15777. */
  15778. Hammer.Instance = function(element, options) {
  15779. var self = this;
  15780. // setup HammerJS window events and register all gestures
  15781. // this also sets up the default options
  15782. setup();
  15783. this.element = element;
  15784. // start/stop detection option
  15785. this.enabled = true;
  15786. // merge options
  15787. this.options = Hammer.utils.extend(
  15788. Hammer.utils.extend({}, Hammer.defaults),
  15789. options || {});
  15790. // add some css to the element to prevent the browser from doing its native behavoir
  15791. if(this.options.stop_browser_behavior) {
  15792. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  15793. }
  15794. // start detection on touchstart
  15795. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  15796. if(self.enabled) {
  15797. Hammer.detection.startDetect(self, ev);
  15798. }
  15799. });
  15800. // return instance
  15801. return this;
  15802. };
  15803. Hammer.Instance.prototype = {
  15804. /**
  15805. * bind events to the instance
  15806. * @param {String} gesture
  15807. * @param {Function} handler
  15808. * @returns {Hammer.Instance}
  15809. */
  15810. on: function onEvent(gesture, handler){
  15811. var gestures = gesture.split(' ');
  15812. for(var t=0; t<gestures.length; t++) {
  15813. this.element.addEventListener(gestures[t], handler, false);
  15814. }
  15815. return this;
  15816. },
  15817. /**
  15818. * unbind events to the instance
  15819. * @param {String} gesture
  15820. * @param {Function} handler
  15821. * @returns {Hammer.Instance}
  15822. */
  15823. off: function offEvent(gesture, handler){
  15824. var gestures = gesture.split(' ');
  15825. for(var t=0; t<gestures.length; t++) {
  15826. this.element.removeEventListener(gestures[t], handler, false);
  15827. }
  15828. return this;
  15829. },
  15830. /**
  15831. * trigger gesture event
  15832. * @param {String} gesture
  15833. * @param {Object} eventData
  15834. * @returns {Hammer.Instance}
  15835. */
  15836. trigger: function triggerEvent(gesture, eventData){
  15837. // create DOM event
  15838. var event = Hammer.DOCUMENT.createEvent('Event');
  15839. event.initEvent(gesture, true, true);
  15840. event.gesture = eventData;
  15841. // trigger on the target if it is in the instance element,
  15842. // this is for event delegation tricks
  15843. var element = this.element;
  15844. if(Hammer.utils.hasParent(eventData.target, element)) {
  15845. element = eventData.target;
  15846. }
  15847. element.dispatchEvent(event);
  15848. return this;
  15849. },
  15850. /**
  15851. * enable of disable hammer.js detection
  15852. * @param {Boolean} state
  15853. * @returns {Hammer.Instance}
  15854. */
  15855. enable: function enable(state) {
  15856. this.enabled = state;
  15857. return this;
  15858. }
  15859. };
  15860. /**
  15861. * this holds the last move event,
  15862. * used to fix empty touchend issue
  15863. * see the onTouch event for an explanation
  15864. * @type {Object}
  15865. */
  15866. var last_move_event = null;
  15867. /**
  15868. * when the mouse is hold down, this is true
  15869. * @type {Boolean}
  15870. */
  15871. var enable_detect = false;
  15872. /**
  15873. * when touch events have been fired, this is true
  15874. * @type {Boolean}
  15875. */
  15876. var touch_triggered = false;
  15877. Hammer.event = {
  15878. /**
  15879. * simple addEventListener
  15880. * @param {HTMLElement} element
  15881. * @param {String} type
  15882. * @param {Function} handler
  15883. */
  15884. bindDom: function(element, type, handler) {
  15885. var types = type.split(' ');
  15886. for(var t=0; t<types.length; t++) {
  15887. element.addEventListener(types[t], handler, false);
  15888. }
  15889. },
  15890. /**
  15891. * touch events with mouse fallback
  15892. * @param {HTMLElement} element
  15893. * @param {String} eventType like Hammer.EVENT_MOVE
  15894. * @param {Function} handler
  15895. */
  15896. onTouch: function onTouch(element, eventType, handler) {
  15897. var self = this;
  15898. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  15899. var sourceEventType = ev.type.toLowerCase();
  15900. // onmouseup, but when touchend has been fired we do nothing.
  15901. // this is for touchdevices which also fire a mouseup on touchend
  15902. if(sourceEventType.match(/mouse/) && touch_triggered) {
  15903. return;
  15904. }
  15905. // mousebutton must be down or a touch event
  15906. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  15907. sourceEventType.match(/pointerdown/) || // pointerevents touch
  15908. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  15909. ){
  15910. enable_detect = true;
  15911. }
  15912. // we are in a touch event, set the touch triggered bool to true,
  15913. // this for the conflicts that may occur on ios and android
  15914. if(sourceEventType.match(/touch|pointer/)) {
  15915. touch_triggered = true;
  15916. }
  15917. // count the total touches on the screen
  15918. var count_touches = 0;
  15919. // when touch has been triggered in this detection session
  15920. // and we are now handling a mouse event, we stop that to prevent conflicts
  15921. if(enable_detect) {
  15922. // update pointerevent
  15923. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  15924. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  15925. }
  15926. // touch
  15927. else if(sourceEventType.match(/touch/)) {
  15928. count_touches = ev.touches.length;
  15929. }
  15930. // mouse
  15931. else if(!touch_triggered) {
  15932. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  15933. }
  15934. // if we are in a end event, but when we remove one touch and
  15935. // we still have enough, set eventType to move
  15936. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  15937. eventType = Hammer.EVENT_MOVE;
  15938. }
  15939. // no touches, force the end event
  15940. else if(!count_touches) {
  15941. eventType = Hammer.EVENT_END;
  15942. }
  15943. // because touchend has no touches, and we often want to use these in our gestures,
  15944. // we send the last move event as our eventData in touchend
  15945. if(!count_touches && last_move_event !== null) {
  15946. ev = last_move_event;
  15947. }
  15948. // store the last move event
  15949. else {
  15950. last_move_event = ev;
  15951. }
  15952. // trigger the handler
  15953. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  15954. // remove pointerevent from list
  15955. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  15956. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  15957. }
  15958. }
  15959. //debug(sourceEventType +" "+ eventType);
  15960. // on the end we reset everything
  15961. if(!count_touches) {
  15962. last_move_event = null;
  15963. enable_detect = false;
  15964. touch_triggered = false;
  15965. Hammer.PointerEvent.reset();
  15966. }
  15967. });
  15968. },
  15969. /**
  15970. * we have different events for each device/browser
  15971. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  15972. */
  15973. determineEventTypes: function determineEventTypes() {
  15974. // determine the eventtype we want to set
  15975. var types;
  15976. // pointerEvents magic
  15977. if(Hammer.HAS_POINTEREVENTS) {
  15978. types = Hammer.PointerEvent.getEvents();
  15979. }
  15980. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  15981. else if(Hammer.NO_MOUSEEVENTS) {
  15982. types = [
  15983. 'touchstart',
  15984. 'touchmove',
  15985. 'touchend touchcancel'];
  15986. }
  15987. // for non pointer events browsers and mixed browsers,
  15988. // like chrome on windows8 touch laptop
  15989. else {
  15990. types = [
  15991. 'touchstart mousedown',
  15992. 'touchmove mousemove',
  15993. 'touchend touchcancel mouseup'];
  15994. }
  15995. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  15996. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  15997. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  15998. },
  15999. /**
  16000. * create touchlist depending on the event
  16001. * @param {Object} ev
  16002. * @param {String} eventType used by the fakemultitouch plugin
  16003. */
  16004. getTouchList: function getTouchList(ev/*, eventType*/) {
  16005. // get the fake pointerEvent touchlist
  16006. if(Hammer.HAS_POINTEREVENTS) {
  16007. return Hammer.PointerEvent.getTouchList();
  16008. }
  16009. // get the touchlist
  16010. else if(ev.touches) {
  16011. return ev.touches;
  16012. }
  16013. // make fake touchlist from mouse position
  16014. else {
  16015. return [{
  16016. identifier: 1,
  16017. pageX: ev.pageX,
  16018. pageY: ev.pageY,
  16019. target: ev.target
  16020. }];
  16021. }
  16022. },
  16023. /**
  16024. * collect event data for Hammer js
  16025. * @param {HTMLElement} element
  16026. * @param {String} eventType like Hammer.EVENT_MOVE
  16027. * @param {Object} eventData
  16028. */
  16029. collectEventData: function collectEventData(element, eventType, ev) {
  16030. var touches = this.getTouchList(ev, eventType);
  16031. // find out pointerType
  16032. var pointerType = Hammer.POINTER_TOUCH;
  16033. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  16034. pointerType = Hammer.POINTER_MOUSE;
  16035. }
  16036. return {
  16037. center : Hammer.utils.getCenter(touches),
  16038. timeStamp : new Date().getTime(),
  16039. target : ev.target,
  16040. touches : touches,
  16041. eventType : eventType,
  16042. pointerType : pointerType,
  16043. srcEvent : ev,
  16044. /**
  16045. * prevent the browser default actions
  16046. * mostly used to disable scrolling of the browser
  16047. */
  16048. preventDefault: function() {
  16049. if(this.srcEvent.preventManipulation) {
  16050. this.srcEvent.preventManipulation();
  16051. }
  16052. if(this.srcEvent.preventDefault) {
  16053. this.srcEvent.preventDefault();
  16054. }
  16055. },
  16056. /**
  16057. * stop bubbling the event up to its parents
  16058. */
  16059. stopPropagation: function() {
  16060. this.srcEvent.stopPropagation();
  16061. },
  16062. /**
  16063. * immediately stop gesture detection
  16064. * might be useful after a swipe was detected
  16065. * @return {*}
  16066. */
  16067. stopDetect: function() {
  16068. return Hammer.detection.stopDetect();
  16069. }
  16070. };
  16071. }
  16072. };
  16073. Hammer.PointerEvent = {
  16074. /**
  16075. * holds all pointers
  16076. * @type {Object}
  16077. */
  16078. pointers: {},
  16079. /**
  16080. * get a list of pointers
  16081. * @returns {Array} touchlist
  16082. */
  16083. getTouchList: function() {
  16084. var self = this;
  16085. var touchlist = [];
  16086. // we can use forEach since pointerEvents only is in IE10
  16087. Object.keys(self.pointers).sort().forEach(function(id) {
  16088. touchlist.push(self.pointers[id]);
  16089. });
  16090. return touchlist;
  16091. },
  16092. /**
  16093. * update the position of a pointer
  16094. * @param {String} type Hammer.EVENT_END
  16095. * @param {Object} pointerEvent
  16096. */
  16097. updatePointer: function(type, pointerEvent) {
  16098. if(type == Hammer.EVENT_END) {
  16099. this.pointers = {};
  16100. }
  16101. else {
  16102. pointerEvent.identifier = pointerEvent.pointerId;
  16103. this.pointers[pointerEvent.pointerId] = pointerEvent;
  16104. }
  16105. return Object.keys(this.pointers).length;
  16106. },
  16107. /**
  16108. * check if ev matches pointertype
  16109. * @param {String} pointerType Hammer.POINTER_MOUSE
  16110. * @param {PointerEvent} ev
  16111. */
  16112. matchType: function(pointerType, ev) {
  16113. if(!ev.pointerType) {
  16114. return false;
  16115. }
  16116. var types = {};
  16117. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  16118. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  16119. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  16120. return types[pointerType];
  16121. },
  16122. /**
  16123. * get events
  16124. */
  16125. getEvents: function() {
  16126. return [
  16127. 'pointerdown MSPointerDown',
  16128. 'pointermove MSPointerMove',
  16129. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  16130. ];
  16131. },
  16132. /**
  16133. * reset the list
  16134. */
  16135. reset: function() {
  16136. this.pointers = {};
  16137. }
  16138. };
  16139. Hammer.utils = {
  16140. /**
  16141. * extend method,
  16142. * also used for cloning when dest is an empty object
  16143. * @param {Object} dest
  16144. * @param {Object} src
  16145. * @parm {Boolean} merge do a merge
  16146. * @returns {Object} dest
  16147. */
  16148. extend: function extend(dest, src, merge) {
  16149. for (var key in src) {
  16150. if(dest[key] !== undefined && merge) {
  16151. continue;
  16152. }
  16153. dest[key] = src[key];
  16154. }
  16155. return dest;
  16156. },
  16157. /**
  16158. * find if a node is in the given parent
  16159. * used for event delegation tricks
  16160. * @param {HTMLElement} node
  16161. * @param {HTMLElement} parent
  16162. * @returns {boolean} has_parent
  16163. */
  16164. hasParent: function(node, parent) {
  16165. while(node){
  16166. if(node == parent) {
  16167. return true;
  16168. }
  16169. node = node.parentNode;
  16170. }
  16171. return false;
  16172. },
  16173. /**
  16174. * get the center of all the touches
  16175. * @param {Array} touches
  16176. * @returns {Object} center
  16177. */
  16178. getCenter: function getCenter(touches) {
  16179. var valuesX = [], valuesY = [];
  16180. for(var t= 0,len=touches.length; t<len; t++) {
  16181. valuesX.push(touches[t].pageX);
  16182. valuesY.push(touches[t].pageY);
  16183. }
  16184. return {
  16185. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  16186. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  16187. };
  16188. },
  16189. /**
  16190. * calculate the velocity between two points
  16191. * @param {Number} delta_time
  16192. * @param {Number} delta_x
  16193. * @param {Number} delta_y
  16194. * @returns {Object} velocity
  16195. */
  16196. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  16197. return {
  16198. x: Math.abs(delta_x / delta_time) || 0,
  16199. y: Math.abs(delta_y / delta_time) || 0
  16200. };
  16201. },
  16202. /**
  16203. * calculate the angle between two coordinates
  16204. * @param {Touch} touch1
  16205. * @param {Touch} touch2
  16206. * @returns {Number} angle
  16207. */
  16208. getAngle: function getAngle(touch1, touch2) {
  16209. var y = touch2.pageY - touch1.pageY,
  16210. x = touch2.pageX - touch1.pageX;
  16211. return Math.atan2(y, x) * 180 / Math.PI;
  16212. },
  16213. /**
  16214. * angle to direction define
  16215. * @param {Touch} touch1
  16216. * @param {Touch} touch2
  16217. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  16218. */
  16219. getDirection: function getDirection(touch1, touch2) {
  16220. var x = Math.abs(touch1.pageX - touch2.pageX),
  16221. y = Math.abs(touch1.pageY - touch2.pageY);
  16222. if(x >= y) {
  16223. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  16224. }
  16225. else {
  16226. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  16227. }
  16228. },
  16229. /**
  16230. * calculate the distance between two touches
  16231. * @param {Touch} touch1
  16232. * @param {Touch} touch2
  16233. * @returns {Number} distance
  16234. */
  16235. getDistance: function getDistance(touch1, touch2) {
  16236. var x = touch2.pageX - touch1.pageX,
  16237. y = touch2.pageY - touch1.pageY;
  16238. return Math.sqrt((x*x) + (y*y));
  16239. },
  16240. /**
  16241. * calculate the scale factor between two touchLists (fingers)
  16242. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  16243. * @param {Array} start
  16244. * @param {Array} end
  16245. * @returns {Number} scale
  16246. */
  16247. getScale: function getScale(start, end) {
  16248. // need two fingers...
  16249. if(start.length >= 2 && end.length >= 2) {
  16250. return this.getDistance(end[0], end[1]) /
  16251. this.getDistance(start[0], start[1]);
  16252. }
  16253. return 1;
  16254. },
  16255. /**
  16256. * calculate the rotation degrees between two touchLists (fingers)
  16257. * @param {Array} start
  16258. * @param {Array} end
  16259. * @returns {Number} rotation
  16260. */
  16261. getRotation: function getRotation(start, end) {
  16262. // need two fingers
  16263. if(start.length >= 2 && end.length >= 2) {
  16264. return this.getAngle(end[1], end[0]) -
  16265. this.getAngle(start[1], start[0]);
  16266. }
  16267. return 0;
  16268. },
  16269. /**
  16270. * boolean if the direction is vertical
  16271. * @param {String} direction
  16272. * @returns {Boolean} is_vertical
  16273. */
  16274. isVertical: function isVertical(direction) {
  16275. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  16276. },
  16277. /**
  16278. * stop browser default behavior with css props
  16279. * @param {HtmlElement} element
  16280. * @param {Object} css_props
  16281. */
  16282. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  16283. var prop,
  16284. vendors = ['webkit','khtml','moz','ms','o',''];
  16285. if(!css_props || !element.style) {
  16286. return;
  16287. }
  16288. // with css properties for modern browsers
  16289. for(var i = 0; i < vendors.length; i++) {
  16290. for(var p in css_props) {
  16291. if(css_props.hasOwnProperty(p)) {
  16292. prop = p;
  16293. // vender prefix at the property
  16294. if(vendors[i]) {
  16295. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  16296. }
  16297. // set the style
  16298. element.style[prop] = css_props[p];
  16299. }
  16300. }
  16301. }
  16302. // also the disable onselectstart
  16303. if(css_props.userSelect == 'none') {
  16304. element.onselectstart = function() {
  16305. return false;
  16306. };
  16307. }
  16308. }
  16309. };
  16310. Hammer.detection = {
  16311. // contains all registred Hammer.gestures in the correct order
  16312. gestures: [],
  16313. // data of the current Hammer.gesture detection session
  16314. current: null,
  16315. // the previous Hammer.gesture session data
  16316. // is a full clone of the previous gesture.current object
  16317. previous: null,
  16318. // when this becomes true, no gestures are fired
  16319. stopped: false,
  16320. /**
  16321. * start Hammer.gesture detection
  16322. * @param {Hammer.Instance} inst
  16323. * @param {Object} eventData
  16324. */
  16325. startDetect: function startDetect(inst, eventData) {
  16326. // already busy with a Hammer.gesture detection on an element
  16327. if(this.current) {
  16328. return;
  16329. }
  16330. this.stopped = false;
  16331. this.current = {
  16332. inst : inst, // reference to HammerInstance we're working for
  16333. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  16334. lastEvent : false, // last eventData
  16335. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  16336. };
  16337. this.detect(eventData);
  16338. },
  16339. /**
  16340. * Hammer.gesture detection
  16341. * @param {Object} eventData
  16342. * @param {Object} eventData
  16343. */
  16344. detect: function detect(eventData) {
  16345. if(!this.current || this.stopped) {
  16346. return;
  16347. }
  16348. // extend event data with calculations about scale, distance etc
  16349. eventData = this.extendEventData(eventData);
  16350. // instance options
  16351. var inst_options = this.current.inst.options;
  16352. // call Hammer.gesture handlers
  16353. for(var g=0,len=this.gestures.length; g<len; g++) {
  16354. var gesture = this.gestures[g];
  16355. // only when the instance options have enabled this gesture
  16356. if(!this.stopped && inst_options[gesture.name] !== false) {
  16357. // if a handler returns false, we stop with the detection
  16358. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  16359. this.stopDetect();
  16360. break;
  16361. }
  16362. }
  16363. }
  16364. // store as previous event event
  16365. if(this.current) {
  16366. this.current.lastEvent = eventData;
  16367. }
  16368. // endevent, but not the last touch, so dont stop
  16369. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  16370. this.stopDetect();
  16371. }
  16372. return eventData;
  16373. },
  16374. /**
  16375. * clear the Hammer.gesture vars
  16376. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  16377. * to stop other Hammer.gestures from being fired
  16378. */
  16379. stopDetect: function stopDetect() {
  16380. // clone current data to the store as the previous gesture
  16381. // used for the double tap gesture, since this is an other gesture detect session
  16382. this.previous = Hammer.utils.extend({}, this.current);
  16383. // reset the current
  16384. this.current = null;
  16385. // stopped!
  16386. this.stopped = true;
  16387. },
  16388. /**
  16389. * extend eventData for Hammer.gestures
  16390. * @param {Object} ev
  16391. * @returns {Object} ev
  16392. */
  16393. extendEventData: function extendEventData(ev) {
  16394. var startEv = this.current.startEvent;
  16395. // if the touches change, set the new touches over the startEvent touches
  16396. // this because touchevents don't have all the touches on touchstart, or the
  16397. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  16398. // but, sometimes it happens that both fingers are touching at the EXACT same time
  16399. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  16400. // extend 1 level deep to get the touchlist with the touch objects
  16401. startEv.touches = [];
  16402. for(var i=0,len=ev.touches.length; i<len; i++) {
  16403. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  16404. }
  16405. }
  16406. var delta_time = ev.timeStamp - startEv.timeStamp,
  16407. delta_x = ev.center.pageX - startEv.center.pageX,
  16408. delta_y = ev.center.pageY - startEv.center.pageY,
  16409. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  16410. Hammer.utils.extend(ev, {
  16411. deltaTime : delta_time,
  16412. deltaX : delta_x,
  16413. deltaY : delta_y,
  16414. velocityX : velocity.x,
  16415. velocityY : velocity.y,
  16416. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  16417. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  16418. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  16419. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  16420. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  16421. startEvent : startEv
  16422. });
  16423. return ev;
  16424. },
  16425. /**
  16426. * register new gesture
  16427. * @param {Object} gesture object, see gestures.js for documentation
  16428. * @returns {Array} gestures
  16429. */
  16430. register: function register(gesture) {
  16431. // add an enable gesture options if there is no given
  16432. var options = gesture.defaults || {};
  16433. if(options[gesture.name] === undefined) {
  16434. options[gesture.name] = true;
  16435. }
  16436. // extend Hammer default options with the Hammer.gesture options
  16437. Hammer.utils.extend(Hammer.defaults, options, true);
  16438. // set its index
  16439. gesture.index = gesture.index || 1000;
  16440. // add Hammer.gesture to the list
  16441. this.gestures.push(gesture);
  16442. // sort the list by index
  16443. this.gestures.sort(function(a, b) {
  16444. if (a.index < b.index) {
  16445. return -1;
  16446. }
  16447. if (a.index > b.index) {
  16448. return 1;
  16449. }
  16450. return 0;
  16451. });
  16452. return this.gestures;
  16453. }
  16454. };
  16455. Hammer.gestures = Hammer.gestures || {};
  16456. /**
  16457. * Custom gestures
  16458. * ==============================
  16459. *
  16460. * Gesture object
  16461. * --------------------
  16462. * The object structure of a gesture:
  16463. *
  16464. * { name: 'mygesture',
  16465. * index: 1337,
  16466. * defaults: {
  16467. * mygesture_option: true
  16468. * }
  16469. * handler: function(type, ev, inst) {
  16470. * // trigger gesture event
  16471. * inst.trigger(this.name, ev);
  16472. * }
  16473. * }
  16474. * @param {String} name
  16475. * this should be the name of the gesture, lowercase
  16476. * it is also being used to disable/enable the gesture per instance config.
  16477. *
  16478. * @param {Number} [index=1000]
  16479. * the index of the gesture, where it is going to be in the stack of gestures detection
  16480. * like when you build an gesture that depends on the drag gesture, it is a good
  16481. * idea to place it after the index of the drag gesture.
  16482. *
  16483. * @param {Object} [defaults={}]
  16484. * the default settings of the gesture. these are added to the instance settings,
  16485. * and can be overruled per instance. you can also add the name of the gesture,
  16486. * but this is also added by default (and set to true).
  16487. *
  16488. * @param {Function} handler
  16489. * this handles the gesture detection of your custom gesture and receives the
  16490. * following arguments:
  16491. *
  16492. * @param {Object} eventData
  16493. * event data containing the following properties:
  16494. * timeStamp {Number} time the event occurred
  16495. * target {HTMLElement} target element
  16496. * touches {Array} touches (fingers, pointers, mouse) on the screen
  16497. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  16498. * center {Object} center position of the touches. contains pageX and pageY
  16499. * deltaTime {Number} the total time of the touches in the screen
  16500. * deltaX {Number} the delta on x axis we haved moved
  16501. * deltaY {Number} the delta on y axis we haved moved
  16502. * velocityX {Number} the velocity on the x
  16503. * velocityY {Number} the velocity on y
  16504. * angle {Number} the angle we are moving
  16505. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  16506. * distance {Number} the distance we haved moved
  16507. * scale {Number} scaling of the touches, needs 2 touches
  16508. * rotation {Number} rotation of the touches, needs 2 touches *
  16509. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  16510. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  16511. * startEvent {Object} contains the same properties as above,
  16512. * but from the first touch. this is used to calculate
  16513. * distances, deltaTime, scaling etc
  16514. *
  16515. * @param {Hammer.Instance} inst
  16516. * the instance we are doing the detection for. you can get the options from
  16517. * the inst.options object and trigger the gesture event by calling inst.trigger
  16518. *
  16519. *
  16520. * Handle gestures
  16521. * --------------------
  16522. * inside the handler you can get/set Hammer.detection.current. This is the current
  16523. * detection session. It has the following properties
  16524. * @param {String} name
  16525. * contains the name of the gesture we have detected. it has not a real function,
  16526. * only to check in other gestures if something is detected.
  16527. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  16528. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  16529. *
  16530. * @readonly
  16531. * @param {Hammer.Instance} inst
  16532. * the instance we do the detection for
  16533. *
  16534. * @readonly
  16535. * @param {Object} startEvent
  16536. * contains the properties of the first gesture detection in this session.
  16537. * Used for calculations about timing, distance, etc.
  16538. *
  16539. * @readonly
  16540. * @param {Object} lastEvent
  16541. * contains all the properties of the last gesture detect in this session.
  16542. *
  16543. * after the gesture detection session has been completed (user has released the screen)
  16544. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  16545. * this is usefull for gestures like doubletap, where you need to know if the
  16546. * previous gesture was a tap
  16547. *
  16548. * options that have been set by the instance can be received by calling inst.options
  16549. *
  16550. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  16551. * The first param is the name of your gesture, the second the event argument
  16552. *
  16553. *
  16554. * Register gestures
  16555. * --------------------
  16556. * When an gesture is added to the Hammer.gestures object, it is auto registered
  16557. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  16558. * manually and pass your gesture object as a param
  16559. *
  16560. */
  16561. /**
  16562. * Hold
  16563. * Touch stays at the same place for x time
  16564. * @events hold
  16565. */
  16566. Hammer.gestures.Hold = {
  16567. name: 'hold',
  16568. index: 10,
  16569. defaults: {
  16570. hold_timeout : 500,
  16571. hold_threshold : 1
  16572. },
  16573. timer: null,
  16574. handler: function holdGesture(ev, inst) {
  16575. switch(ev.eventType) {
  16576. case Hammer.EVENT_START:
  16577. // clear any running timers
  16578. clearTimeout(this.timer);
  16579. // set the gesture so we can check in the timeout if it still is
  16580. Hammer.detection.current.name = this.name;
  16581. // set timer and if after the timeout it still is hold,
  16582. // we trigger the hold event
  16583. this.timer = setTimeout(function() {
  16584. if(Hammer.detection.current.name == 'hold') {
  16585. inst.trigger('hold', ev);
  16586. }
  16587. }, inst.options.hold_timeout);
  16588. break;
  16589. // when you move or end we clear the timer
  16590. case Hammer.EVENT_MOVE:
  16591. if(ev.distance > inst.options.hold_threshold) {
  16592. clearTimeout(this.timer);
  16593. }
  16594. break;
  16595. case Hammer.EVENT_END:
  16596. clearTimeout(this.timer);
  16597. break;
  16598. }
  16599. }
  16600. };
  16601. /**
  16602. * Tap/DoubleTap
  16603. * Quick touch at a place or double at the same place
  16604. * @events tap, doubletap
  16605. */
  16606. Hammer.gestures.Tap = {
  16607. name: 'tap',
  16608. index: 100,
  16609. defaults: {
  16610. tap_max_touchtime : 250,
  16611. tap_max_distance : 10,
  16612. tap_always : true,
  16613. doubletap_distance : 20,
  16614. doubletap_interval : 300
  16615. },
  16616. handler: function tapGesture(ev, inst) {
  16617. if(ev.eventType == Hammer.EVENT_END) {
  16618. // previous gesture, for the double tap since these are two different gesture detections
  16619. var prev = Hammer.detection.previous,
  16620. did_doubletap = false;
  16621. // when the touchtime is higher then the max touch time
  16622. // or when the moving distance is too much
  16623. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  16624. ev.distance > inst.options.tap_max_distance) {
  16625. return;
  16626. }
  16627. // check if double tap
  16628. if(prev && prev.name == 'tap' &&
  16629. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  16630. ev.distance < inst.options.doubletap_distance) {
  16631. inst.trigger('doubletap', ev);
  16632. did_doubletap = true;
  16633. }
  16634. // do a single tap
  16635. if(!did_doubletap || inst.options.tap_always) {
  16636. Hammer.detection.current.name = 'tap';
  16637. inst.trigger(Hammer.detection.current.name, ev);
  16638. }
  16639. }
  16640. }
  16641. };
  16642. /**
  16643. * Swipe
  16644. * triggers swipe events when the end velocity is above the threshold
  16645. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  16646. */
  16647. Hammer.gestures.Swipe = {
  16648. name: 'swipe',
  16649. index: 40,
  16650. defaults: {
  16651. // set 0 for unlimited, but this can conflict with transform
  16652. swipe_max_touches : 1,
  16653. swipe_velocity : 0.7
  16654. },
  16655. handler: function swipeGesture(ev, inst) {
  16656. if(ev.eventType == Hammer.EVENT_END) {
  16657. // max touches
  16658. if(inst.options.swipe_max_touches > 0 &&
  16659. ev.touches.length > inst.options.swipe_max_touches) {
  16660. return;
  16661. }
  16662. // when the distance we moved is too small we skip this gesture
  16663. // or we can be already in dragging
  16664. if(ev.velocityX > inst.options.swipe_velocity ||
  16665. ev.velocityY > inst.options.swipe_velocity) {
  16666. // trigger swipe events
  16667. inst.trigger(this.name, ev);
  16668. inst.trigger(this.name + ev.direction, ev);
  16669. }
  16670. }
  16671. }
  16672. };
  16673. /**
  16674. * Drag
  16675. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  16676. * moving left and right is a good practice. When all the drag events are blocking
  16677. * you disable scrolling on that area.
  16678. * @events drag, drapleft, dragright, dragup, dragdown
  16679. */
  16680. Hammer.gestures.Drag = {
  16681. name: 'drag',
  16682. index: 50,
  16683. defaults: {
  16684. drag_min_distance : 10,
  16685. // set 0 for unlimited, but this can conflict with transform
  16686. drag_max_touches : 1,
  16687. // prevent default browser behavior when dragging occurs
  16688. // be careful with it, it makes the element a blocking element
  16689. // when you are using the drag gesture, it is a good practice to set this true
  16690. drag_block_horizontal : false,
  16691. drag_block_vertical : false,
  16692. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  16693. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  16694. drag_lock_to_axis : false,
  16695. // drag lock only kicks in when distance > drag_lock_min_distance
  16696. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  16697. drag_lock_min_distance : 25
  16698. },
  16699. triggered: false,
  16700. handler: function dragGesture(ev, inst) {
  16701. // current gesture isnt drag, but dragged is true
  16702. // this means an other gesture is busy. now call dragend
  16703. if(Hammer.detection.current.name != this.name && this.triggered) {
  16704. inst.trigger(this.name +'end', ev);
  16705. this.triggered = false;
  16706. return;
  16707. }
  16708. // max touches
  16709. if(inst.options.drag_max_touches > 0 &&
  16710. ev.touches.length > inst.options.drag_max_touches) {
  16711. return;
  16712. }
  16713. switch(ev.eventType) {
  16714. case Hammer.EVENT_START:
  16715. this.triggered = false;
  16716. break;
  16717. case Hammer.EVENT_MOVE:
  16718. // when the distance we moved is too small we skip this gesture
  16719. // or we can be already in dragging
  16720. if(ev.distance < inst.options.drag_min_distance &&
  16721. Hammer.detection.current.name != this.name) {
  16722. return;
  16723. }
  16724. // we are dragging!
  16725. Hammer.detection.current.name = this.name;
  16726. // lock drag to axis?
  16727. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  16728. ev.drag_locked_to_axis = true;
  16729. }
  16730. var last_direction = Hammer.detection.current.lastEvent.direction;
  16731. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  16732. // keep direction on the axis that the drag gesture started on
  16733. if(Hammer.utils.isVertical(last_direction)) {
  16734. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  16735. }
  16736. else {
  16737. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  16738. }
  16739. }
  16740. // first time, trigger dragstart event
  16741. if(!this.triggered) {
  16742. inst.trigger(this.name +'start', ev);
  16743. this.triggered = true;
  16744. }
  16745. // trigger normal event
  16746. inst.trigger(this.name, ev);
  16747. // direction event, like dragdown
  16748. inst.trigger(this.name + ev.direction, ev);
  16749. // block the browser events
  16750. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  16751. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  16752. ev.preventDefault();
  16753. }
  16754. break;
  16755. case Hammer.EVENT_END:
  16756. // trigger dragend
  16757. if(this.triggered) {
  16758. inst.trigger(this.name +'end', ev);
  16759. }
  16760. this.triggered = false;
  16761. break;
  16762. }
  16763. }
  16764. };
  16765. /**
  16766. * Transform
  16767. * User want to scale or rotate with 2 fingers
  16768. * @events transform, pinch, pinchin, pinchout, rotate
  16769. */
  16770. Hammer.gestures.Transform = {
  16771. name: 'transform',
  16772. index: 45,
  16773. defaults: {
  16774. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  16775. transform_min_scale : 0.01,
  16776. // rotation in degrees
  16777. transform_min_rotation : 1,
  16778. // prevent default browser behavior when two touches are on the screen
  16779. // but it makes the element a blocking element
  16780. // when you are using the transform gesture, it is a good practice to set this true
  16781. transform_always_block : false
  16782. },
  16783. triggered: false,
  16784. handler: function transformGesture(ev, inst) {
  16785. // current gesture isnt drag, but dragged is true
  16786. // this means an other gesture is busy. now call dragend
  16787. if(Hammer.detection.current.name != this.name && this.triggered) {
  16788. inst.trigger(this.name +'end', ev);
  16789. this.triggered = false;
  16790. return;
  16791. }
  16792. // atleast multitouch
  16793. if(ev.touches.length < 2) {
  16794. return;
  16795. }
  16796. // prevent default when two fingers are on the screen
  16797. if(inst.options.transform_always_block) {
  16798. ev.preventDefault();
  16799. }
  16800. switch(ev.eventType) {
  16801. case Hammer.EVENT_START:
  16802. this.triggered = false;
  16803. break;
  16804. case Hammer.EVENT_MOVE:
  16805. var scale_threshold = Math.abs(1-ev.scale);
  16806. var rotation_threshold = Math.abs(ev.rotation);
  16807. // when the distance we moved is too small we skip this gesture
  16808. // or we can be already in dragging
  16809. if(scale_threshold < inst.options.transform_min_scale &&
  16810. rotation_threshold < inst.options.transform_min_rotation) {
  16811. return;
  16812. }
  16813. // we are transforming!
  16814. Hammer.detection.current.name = this.name;
  16815. // first time, trigger dragstart event
  16816. if(!this.triggered) {
  16817. inst.trigger(this.name +'start', ev);
  16818. this.triggered = true;
  16819. }
  16820. inst.trigger(this.name, ev); // basic transform event
  16821. // trigger rotate event
  16822. if(rotation_threshold > inst.options.transform_min_rotation) {
  16823. inst.trigger('rotate', ev);
  16824. }
  16825. // trigger pinch event
  16826. if(scale_threshold > inst.options.transform_min_scale) {
  16827. inst.trigger('pinch', ev);
  16828. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  16829. }
  16830. break;
  16831. case Hammer.EVENT_END:
  16832. // trigger dragend
  16833. if(this.triggered) {
  16834. inst.trigger(this.name +'end', ev);
  16835. }
  16836. this.triggered = false;
  16837. break;
  16838. }
  16839. }
  16840. };
  16841. /**
  16842. * Touch
  16843. * Called as first, tells the user has touched the screen
  16844. * @events touch
  16845. */
  16846. Hammer.gestures.Touch = {
  16847. name: 'touch',
  16848. index: -Infinity,
  16849. defaults: {
  16850. // call preventDefault at touchstart, and makes the element blocking by
  16851. // disabling the scrolling of the page, but it improves gestures like
  16852. // transforming and dragging.
  16853. // be careful with using this, it can be very annoying for users to be stuck
  16854. // on the page
  16855. prevent_default: false,
  16856. // disable mouse events, so only touch (or pen!) input triggers events
  16857. prevent_mouseevents: false
  16858. },
  16859. handler: function touchGesture(ev, inst) {
  16860. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  16861. ev.stopDetect();
  16862. return;
  16863. }
  16864. if(inst.options.prevent_default) {
  16865. ev.preventDefault();
  16866. }
  16867. if(ev.eventType == Hammer.EVENT_START) {
  16868. inst.trigger(this.name, ev);
  16869. }
  16870. }
  16871. };
  16872. /**
  16873. * Release
  16874. * Called as last, tells the user has released the screen
  16875. * @events release
  16876. */
  16877. Hammer.gestures.Release = {
  16878. name: 'release',
  16879. index: Infinity,
  16880. handler: function releaseGesture(ev, inst) {
  16881. if(ev.eventType == Hammer.EVENT_END) {
  16882. inst.trigger(this.name, ev);
  16883. }
  16884. }
  16885. };
  16886. // node export
  16887. if(typeof module === 'object' && typeof module.exports === 'object'){
  16888. module.exports = Hammer;
  16889. }
  16890. // just window export
  16891. else {
  16892. window.Hammer = Hammer;
  16893. // requireJS module definition
  16894. if(typeof window.define === 'function' && window.define.amd) {
  16895. window.define('hammer', [], function() {
  16896. return Hammer;
  16897. });
  16898. }
  16899. }
  16900. })(this);
  16901. },{}],4:[function(require,module,exports){
  16902. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  16903. //! version : 2.6.0
  16904. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  16905. //! license : MIT
  16906. //! momentjs.com
  16907. (function (undefined) {
  16908. /************************************
  16909. Constants
  16910. ************************************/
  16911. var moment,
  16912. VERSION = "2.6.0",
  16913. // the global-scope this is NOT the global object in Node.js
  16914. globalScope = typeof global !== 'undefined' ? global : this,
  16915. oldGlobalMoment,
  16916. round = Math.round,
  16917. i,
  16918. YEAR = 0,
  16919. MONTH = 1,
  16920. DATE = 2,
  16921. HOUR = 3,
  16922. MINUTE = 4,
  16923. SECOND = 5,
  16924. MILLISECOND = 6,
  16925. // internal storage for language config files
  16926. languages = {},
  16927. // moment internal properties
  16928. momentProperties = {
  16929. _isAMomentObject: null,
  16930. _i : null,
  16931. _f : null,
  16932. _l : null,
  16933. _strict : null,
  16934. _isUTC : null,
  16935. _offset : null, // optional. Combine with _isUTC
  16936. _pf : null,
  16937. _lang : null // optional
  16938. },
  16939. // check for nodeJS
  16940. hasModule = (typeof module !== 'undefined' && module.exports),
  16941. // ASP.NET json date format regex
  16942. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  16943. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  16944. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  16945. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  16946. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  16947. // format tokens
  16948. 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,
  16949. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  16950. // parsing token regexes
  16951. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  16952. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  16953. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  16954. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  16955. parseTokenDigits = /\d+/, // nonzero number of digits
  16956. 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.
  16957. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  16958. parseTokenT = /T/i, // T (ISO separator)
  16959. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  16960. parseTokenOrdinal = /\d{1,2}/,
  16961. //strict parsing regexes
  16962. parseTokenOneDigit = /\d/, // 0 - 9
  16963. parseTokenTwoDigits = /\d\d/, // 00 - 99
  16964. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  16965. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  16966. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  16967. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  16968. // iso 8601 regex
  16969. // 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)
  16970. 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)?)?$/,
  16971. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  16972. isoDates = [
  16973. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  16974. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  16975. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  16976. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  16977. ['YYYY-DDD', /\d{4}-\d{3}/]
  16978. ],
  16979. // iso time formats and regexes
  16980. isoTimes = [
  16981. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  16982. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  16983. ['HH:mm', /(T| )\d\d:\d\d/],
  16984. ['HH', /(T| )\d\d/]
  16985. ],
  16986. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  16987. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  16988. // getter and setter names
  16989. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  16990. unitMillisecondFactors = {
  16991. 'Milliseconds' : 1,
  16992. 'Seconds' : 1e3,
  16993. 'Minutes' : 6e4,
  16994. 'Hours' : 36e5,
  16995. 'Days' : 864e5,
  16996. 'Months' : 2592e6,
  16997. 'Years' : 31536e6
  16998. },
  16999. unitAliases = {
  17000. ms : 'millisecond',
  17001. s : 'second',
  17002. m : 'minute',
  17003. h : 'hour',
  17004. d : 'day',
  17005. D : 'date',
  17006. w : 'week',
  17007. W : 'isoWeek',
  17008. M : 'month',
  17009. Q : 'quarter',
  17010. y : 'year',
  17011. DDD : 'dayOfYear',
  17012. e : 'weekday',
  17013. E : 'isoWeekday',
  17014. gg: 'weekYear',
  17015. GG: 'isoWeekYear'
  17016. },
  17017. camelFunctions = {
  17018. dayofyear : 'dayOfYear',
  17019. isoweekday : 'isoWeekday',
  17020. isoweek : 'isoWeek',
  17021. weekyear : 'weekYear',
  17022. isoweekyear : 'isoWeekYear'
  17023. },
  17024. // format function strings
  17025. formatFunctions = {},
  17026. // tokens to ordinalize and pad
  17027. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  17028. paddedTokens = 'M D H h m s w W'.split(' '),
  17029. formatTokenFunctions = {
  17030. M : function () {
  17031. return this.month() + 1;
  17032. },
  17033. MMM : function (format) {
  17034. return this.lang().monthsShort(this, format);
  17035. },
  17036. MMMM : function (format) {
  17037. return this.lang().months(this, format);
  17038. },
  17039. D : function () {
  17040. return this.date();
  17041. },
  17042. DDD : function () {
  17043. return this.dayOfYear();
  17044. },
  17045. d : function () {
  17046. return this.day();
  17047. },
  17048. dd : function (format) {
  17049. return this.lang().weekdaysMin(this, format);
  17050. },
  17051. ddd : function (format) {
  17052. return this.lang().weekdaysShort(this, format);
  17053. },
  17054. dddd : function (format) {
  17055. return this.lang().weekdays(this, format);
  17056. },
  17057. w : function () {
  17058. return this.week();
  17059. },
  17060. W : function () {
  17061. return this.isoWeek();
  17062. },
  17063. YY : function () {
  17064. return leftZeroFill(this.year() % 100, 2);
  17065. },
  17066. YYYY : function () {
  17067. return leftZeroFill(this.year(), 4);
  17068. },
  17069. YYYYY : function () {
  17070. return leftZeroFill(this.year(), 5);
  17071. },
  17072. YYYYYY : function () {
  17073. var y = this.year(), sign = y >= 0 ? '+' : '-';
  17074. return sign + leftZeroFill(Math.abs(y), 6);
  17075. },
  17076. gg : function () {
  17077. return leftZeroFill(this.weekYear() % 100, 2);
  17078. },
  17079. gggg : function () {
  17080. return leftZeroFill(this.weekYear(), 4);
  17081. },
  17082. ggggg : function () {
  17083. return leftZeroFill(this.weekYear(), 5);
  17084. },
  17085. GG : function () {
  17086. return leftZeroFill(this.isoWeekYear() % 100, 2);
  17087. },
  17088. GGGG : function () {
  17089. return leftZeroFill(this.isoWeekYear(), 4);
  17090. },
  17091. GGGGG : function () {
  17092. return leftZeroFill(this.isoWeekYear(), 5);
  17093. },
  17094. e : function () {
  17095. return this.weekday();
  17096. },
  17097. E : function () {
  17098. return this.isoWeekday();
  17099. },
  17100. a : function () {
  17101. return this.lang().meridiem(this.hours(), this.minutes(), true);
  17102. },
  17103. A : function () {
  17104. return this.lang().meridiem(this.hours(), this.minutes(), false);
  17105. },
  17106. H : function () {
  17107. return this.hours();
  17108. },
  17109. h : function () {
  17110. return this.hours() % 12 || 12;
  17111. },
  17112. m : function () {
  17113. return this.minutes();
  17114. },
  17115. s : function () {
  17116. return this.seconds();
  17117. },
  17118. S : function () {
  17119. return toInt(this.milliseconds() / 100);
  17120. },
  17121. SS : function () {
  17122. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  17123. },
  17124. SSS : function () {
  17125. return leftZeroFill(this.milliseconds(), 3);
  17126. },
  17127. SSSS : function () {
  17128. return leftZeroFill(this.milliseconds(), 3);
  17129. },
  17130. Z : function () {
  17131. var a = -this.zone(),
  17132. b = "+";
  17133. if (a < 0) {
  17134. a = -a;
  17135. b = "-";
  17136. }
  17137. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  17138. },
  17139. ZZ : function () {
  17140. var a = -this.zone(),
  17141. b = "+";
  17142. if (a < 0) {
  17143. a = -a;
  17144. b = "-";
  17145. }
  17146. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  17147. },
  17148. z : function () {
  17149. return this.zoneAbbr();
  17150. },
  17151. zz : function () {
  17152. return this.zoneName();
  17153. },
  17154. X : function () {
  17155. return this.unix();
  17156. },
  17157. Q : function () {
  17158. return this.quarter();
  17159. }
  17160. },
  17161. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  17162. function defaultParsingFlags() {
  17163. // We need to deep clone this object, and es5 standard is not very
  17164. // helpful.
  17165. return {
  17166. empty : false,
  17167. unusedTokens : [],
  17168. unusedInput : [],
  17169. overflow : -2,
  17170. charsLeftOver : 0,
  17171. nullInput : false,
  17172. invalidMonth : null,
  17173. invalidFormat : false,
  17174. userInvalidated : false,
  17175. iso: false
  17176. };
  17177. }
  17178. function deprecate(msg, fn) {
  17179. var firstTime = true;
  17180. function printMsg() {
  17181. if (moment.suppressDeprecationWarnings === false &&
  17182. typeof console !== 'undefined' && console.warn) {
  17183. console.warn("Deprecation warning: " + msg);
  17184. }
  17185. }
  17186. return extend(function () {
  17187. if (firstTime) {
  17188. printMsg();
  17189. firstTime = false;
  17190. }
  17191. return fn.apply(this, arguments);
  17192. }, fn);
  17193. }
  17194. function padToken(func, count) {
  17195. return function (a) {
  17196. return leftZeroFill(func.call(this, a), count);
  17197. };
  17198. }
  17199. function ordinalizeToken(func, period) {
  17200. return function (a) {
  17201. return this.lang().ordinal(func.call(this, a), period);
  17202. };
  17203. }
  17204. while (ordinalizeTokens.length) {
  17205. i = ordinalizeTokens.pop();
  17206. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  17207. }
  17208. while (paddedTokens.length) {
  17209. i = paddedTokens.pop();
  17210. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  17211. }
  17212. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  17213. /************************************
  17214. Constructors
  17215. ************************************/
  17216. function Language() {
  17217. }
  17218. // Moment prototype object
  17219. function Moment(config) {
  17220. checkOverflow(config);
  17221. extend(this, config);
  17222. }
  17223. // Duration Constructor
  17224. function Duration(duration) {
  17225. var normalizedInput = normalizeObjectUnits(duration),
  17226. years = normalizedInput.year || 0,
  17227. quarters = normalizedInput.quarter || 0,
  17228. months = normalizedInput.month || 0,
  17229. weeks = normalizedInput.week || 0,
  17230. days = normalizedInput.day || 0,
  17231. hours = normalizedInput.hour || 0,
  17232. minutes = normalizedInput.minute || 0,
  17233. seconds = normalizedInput.second || 0,
  17234. milliseconds = normalizedInput.millisecond || 0;
  17235. // representation for dateAddRemove
  17236. this._milliseconds = +milliseconds +
  17237. seconds * 1e3 + // 1000
  17238. minutes * 6e4 + // 1000 * 60
  17239. hours * 36e5; // 1000 * 60 * 60
  17240. // Because of dateAddRemove treats 24 hours as different from a
  17241. // day when working around DST, we need to store them separately
  17242. this._days = +days +
  17243. weeks * 7;
  17244. // It is impossible translate months into days without knowing
  17245. // which months you are are talking about, so we have to store
  17246. // it separately.
  17247. this._months = +months +
  17248. quarters * 3 +
  17249. years * 12;
  17250. this._data = {};
  17251. this._bubble();
  17252. }
  17253. /************************************
  17254. Helpers
  17255. ************************************/
  17256. function extend(a, b) {
  17257. for (var i in b) {
  17258. if (b.hasOwnProperty(i)) {
  17259. a[i] = b[i];
  17260. }
  17261. }
  17262. if (b.hasOwnProperty("toString")) {
  17263. a.toString = b.toString;
  17264. }
  17265. if (b.hasOwnProperty("valueOf")) {
  17266. a.valueOf = b.valueOf;
  17267. }
  17268. return a;
  17269. }
  17270. function cloneMoment(m) {
  17271. var result = {}, i;
  17272. for (i in m) {
  17273. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  17274. result[i] = m[i];
  17275. }
  17276. }
  17277. return result;
  17278. }
  17279. function absRound(number) {
  17280. if (number < 0) {
  17281. return Math.ceil(number);
  17282. } else {
  17283. return Math.floor(number);
  17284. }
  17285. }
  17286. // left zero fill a number
  17287. // see http://jsperf.com/left-zero-filling for performance comparison
  17288. function leftZeroFill(number, targetLength, forceSign) {
  17289. var output = '' + Math.abs(number),
  17290. sign = number >= 0;
  17291. while (output.length < targetLength) {
  17292. output = '0' + output;
  17293. }
  17294. return (sign ? (forceSign ? '+' : '') : '-') + output;
  17295. }
  17296. // helper function for _.addTime and _.subtractTime
  17297. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  17298. var milliseconds = duration._milliseconds,
  17299. days = duration._days,
  17300. months = duration._months;
  17301. updateOffset = updateOffset == null ? true : updateOffset;
  17302. if (milliseconds) {
  17303. mom._d.setTime(+mom._d + milliseconds * isAdding);
  17304. }
  17305. if (days) {
  17306. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  17307. }
  17308. if (months) {
  17309. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  17310. }
  17311. if (updateOffset) {
  17312. moment.updateOffset(mom, days || months);
  17313. }
  17314. }
  17315. // check if is an array
  17316. function isArray(input) {
  17317. return Object.prototype.toString.call(input) === '[object Array]';
  17318. }
  17319. function isDate(input) {
  17320. return Object.prototype.toString.call(input) === '[object Date]' ||
  17321. input instanceof Date;
  17322. }
  17323. // compare two arrays, return the number of differences
  17324. function compareArrays(array1, array2, dontConvert) {
  17325. var len = Math.min(array1.length, array2.length),
  17326. lengthDiff = Math.abs(array1.length - array2.length),
  17327. diffs = 0,
  17328. i;
  17329. for (i = 0; i < len; i++) {
  17330. if ((dontConvert && array1[i] !== array2[i]) ||
  17331. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  17332. diffs++;
  17333. }
  17334. }
  17335. return diffs + lengthDiff;
  17336. }
  17337. function normalizeUnits(units) {
  17338. if (units) {
  17339. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  17340. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  17341. }
  17342. return units;
  17343. }
  17344. function normalizeObjectUnits(inputObject) {
  17345. var normalizedInput = {},
  17346. normalizedProp,
  17347. prop;
  17348. for (prop in inputObject) {
  17349. if (inputObject.hasOwnProperty(prop)) {
  17350. normalizedProp = normalizeUnits(prop);
  17351. if (normalizedProp) {
  17352. normalizedInput[normalizedProp] = inputObject[prop];
  17353. }
  17354. }
  17355. }
  17356. return normalizedInput;
  17357. }
  17358. function makeList(field) {
  17359. var count, setter;
  17360. if (field.indexOf('week') === 0) {
  17361. count = 7;
  17362. setter = 'day';
  17363. }
  17364. else if (field.indexOf('month') === 0) {
  17365. count = 12;
  17366. setter = 'month';
  17367. }
  17368. else {
  17369. return;
  17370. }
  17371. moment[field] = function (format, index) {
  17372. var i, getter,
  17373. method = moment.fn._lang[field],
  17374. results = [];
  17375. if (typeof format === 'number') {
  17376. index = format;
  17377. format = undefined;
  17378. }
  17379. getter = function (i) {
  17380. var m = moment().utc().set(setter, i);
  17381. return method.call(moment.fn._lang, m, format || '');
  17382. };
  17383. if (index != null) {
  17384. return getter(index);
  17385. }
  17386. else {
  17387. for (i = 0; i < count; i++) {
  17388. results.push(getter(i));
  17389. }
  17390. return results;
  17391. }
  17392. };
  17393. }
  17394. function toInt(argumentForCoercion) {
  17395. var coercedNumber = +argumentForCoercion,
  17396. value = 0;
  17397. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  17398. if (coercedNumber >= 0) {
  17399. value = Math.floor(coercedNumber);
  17400. } else {
  17401. value = Math.ceil(coercedNumber);
  17402. }
  17403. }
  17404. return value;
  17405. }
  17406. function daysInMonth(year, month) {
  17407. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  17408. }
  17409. function weeksInYear(year, dow, doy) {
  17410. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  17411. }
  17412. function daysInYear(year) {
  17413. return isLeapYear(year) ? 366 : 365;
  17414. }
  17415. function isLeapYear(year) {
  17416. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  17417. }
  17418. function checkOverflow(m) {
  17419. var overflow;
  17420. if (m._a && m._pf.overflow === -2) {
  17421. overflow =
  17422. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  17423. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  17424. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  17425. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  17426. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  17427. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  17428. -1;
  17429. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  17430. overflow = DATE;
  17431. }
  17432. m._pf.overflow = overflow;
  17433. }
  17434. }
  17435. function isValid(m) {
  17436. if (m._isValid == null) {
  17437. m._isValid = !isNaN(m._d.getTime()) &&
  17438. m._pf.overflow < 0 &&
  17439. !m._pf.empty &&
  17440. !m._pf.invalidMonth &&
  17441. !m._pf.nullInput &&
  17442. !m._pf.invalidFormat &&
  17443. !m._pf.userInvalidated;
  17444. if (m._strict) {
  17445. m._isValid = m._isValid &&
  17446. m._pf.charsLeftOver === 0 &&
  17447. m._pf.unusedTokens.length === 0;
  17448. }
  17449. }
  17450. return m._isValid;
  17451. }
  17452. function normalizeLanguage(key) {
  17453. return key ? key.toLowerCase().replace('_', '-') : key;
  17454. }
  17455. // Return a moment from input, that is local/utc/zone equivalent to model.
  17456. function makeAs(input, model) {
  17457. return model._isUTC ? moment(input).zone(model._offset || 0) :
  17458. moment(input).local();
  17459. }
  17460. /************************************
  17461. Languages
  17462. ************************************/
  17463. extend(Language.prototype, {
  17464. set : function (config) {
  17465. var prop, i;
  17466. for (i in config) {
  17467. prop = config[i];
  17468. if (typeof prop === 'function') {
  17469. this[i] = prop;
  17470. } else {
  17471. this['_' + i] = prop;
  17472. }
  17473. }
  17474. },
  17475. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  17476. months : function (m) {
  17477. return this._months[m.month()];
  17478. },
  17479. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  17480. monthsShort : function (m) {
  17481. return this._monthsShort[m.month()];
  17482. },
  17483. monthsParse : function (monthName) {
  17484. var i, mom, regex;
  17485. if (!this._monthsParse) {
  17486. this._monthsParse = [];
  17487. }
  17488. for (i = 0; i < 12; i++) {
  17489. // make the regex if we don't have it already
  17490. if (!this._monthsParse[i]) {
  17491. mom = moment.utc([2000, i]);
  17492. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  17493. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17494. }
  17495. // test the regex
  17496. if (this._monthsParse[i].test(monthName)) {
  17497. return i;
  17498. }
  17499. }
  17500. },
  17501. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  17502. weekdays : function (m) {
  17503. return this._weekdays[m.day()];
  17504. },
  17505. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  17506. weekdaysShort : function (m) {
  17507. return this._weekdaysShort[m.day()];
  17508. },
  17509. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  17510. weekdaysMin : function (m) {
  17511. return this._weekdaysMin[m.day()];
  17512. },
  17513. weekdaysParse : function (weekdayName) {
  17514. var i, mom, regex;
  17515. if (!this._weekdaysParse) {
  17516. this._weekdaysParse = [];
  17517. }
  17518. for (i = 0; i < 7; i++) {
  17519. // make the regex if we don't have it already
  17520. if (!this._weekdaysParse[i]) {
  17521. mom = moment([2000, 1]).day(i);
  17522. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  17523. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  17524. }
  17525. // test the regex
  17526. if (this._weekdaysParse[i].test(weekdayName)) {
  17527. return i;
  17528. }
  17529. }
  17530. },
  17531. _longDateFormat : {
  17532. LT : "h:mm A",
  17533. L : "MM/DD/YYYY",
  17534. LL : "MMMM D YYYY",
  17535. LLL : "MMMM D YYYY LT",
  17536. LLLL : "dddd, MMMM D YYYY LT"
  17537. },
  17538. longDateFormat : function (key) {
  17539. var output = this._longDateFormat[key];
  17540. if (!output && this._longDateFormat[key.toUpperCase()]) {
  17541. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  17542. return val.slice(1);
  17543. });
  17544. this._longDateFormat[key] = output;
  17545. }
  17546. return output;
  17547. },
  17548. isPM : function (input) {
  17549. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  17550. // Using charAt should be more compatible.
  17551. return ((input + '').toLowerCase().charAt(0) === 'p');
  17552. },
  17553. _meridiemParse : /[ap]\.?m?\.?/i,
  17554. meridiem : function (hours, minutes, isLower) {
  17555. if (hours > 11) {
  17556. return isLower ? 'pm' : 'PM';
  17557. } else {
  17558. return isLower ? 'am' : 'AM';
  17559. }
  17560. },
  17561. _calendar : {
  17562. sameDay : '[Today at] LT',
  17563. nextDay : '[Tomorrow at] LT',
  17564. nextWeek : 'dddd [at] LT',
  17565. lastDay : '[Yesterday at] LT',
  17566. lastWeek : '[Last] dddd [at] LT',
  17567. sameElse : 'L'
  17568. },
  17569. calendar : function (key, mom) {
  17570. var output = this._calendar[key];
  17571. return typeof output === 'function' ? output.apply(mom) : output;
  17572. },
  17573. _relativeTime : {
  17574. future : "in %s",
  17575. past : "%s ago",
  17576. s : "a few seconds",
  17577. m : "a minute",
  17578. mm : "%d minutes",
  17579. h : "an hour",
  17580. hh : "%d hours",
  17581. d : "a day",
  17582. dd : "%d days",
  17583. M : "a month",
  17584. MM : "%d months",
  17585. y : "a year",
  17586. yy : "%d years"
  17587. },
  17588. relativeTime : function (number, withoutSuffix, string, isFuture) {
  17589. var output = this._relativeTime[string];
  17590. return (typeof output === 'function') ?
  17591. output(number, withoutSuffix, string, isFuture) :
  17592. output.replace(/%d/i, number);
  17593. },
  17594. pastFuture : function (diff, output) {
  17595. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  17596. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  17597. },
  17598. ordinal : function (number) {
  17599. return this._ordinal.replace("%d", number);
  17600. },
  17601. _ordinal : "%d",
  17602. preparse : function (string) {
  17603. return string;
  17604. },
  17605. postformat : function (string) {
  17606. return string;
  17607. },
  17608. week : function (mom) {
  17609. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  17610. },
  17611. _week : {
  17612. dow : 0, // Sunday is the first day of the week.
  17613. doy : 6 // The week that contains Jan 1st is the first week of the year.
  17614. },
  17615. _invalidDate: 'Invalid date',
  17616. invalidDate: function () {
  17617. return this._invalidDate;
  17618. }
  17619. });
  17620. // Loads a language definition into the `languages` cache. The function
  17621. // takes a key and optionally values. If not in the browser and no values
  17622. // are provided, it will load the language file module. As a convenience,
  17623. // this function also returns the language values.
  17624. function loadLang(key, values) {
  17625. values.abbr = key;
  17626. if (!languages[key]) {
  17627. languages[key] = new Language();
  17628. }
  17629. languages[key].set(values);
  17630. return languages[key];
  17631. }
  17632. // Remove a language from the `languages` cache. Mostly useful in tests.
  17633. function unloadLang(key) {
  17634. delete languages[key];
  17635. }
  17636. // Determines which language definition to use and returns it.
  17637. //
  17638. // With no parameters, it will return the global language. If you
  17639. // pass in a language key, such as 'en', it will return the
  17640. // definition for 'en', so long as 'en' has already been loaded using
  17641. // moment.lang.
  17642. function getLangDefinition(key) {
  17643. var i = 0, j, lang, next, split,
  17644. get = function (k) {
  17645. if (!languages[k] && hasModule) {
  17646. try {
  17647. require('./lang/' + k);
  17648. } catch (e) { }
  17649. }
  17650. return languages[k];
  17651. };
  17652. if (!key) {
  17653. return moment.fn._lang;
  17654. }
  17655. if (!isArray(key)) {
  17656. //short-circuit everything else
  17657. lang = get(key);
  17658. if (lang) {
  17659. return lang;
  17660. }
  17661. key = [key];
  17662. }
  17663. //pick the language from the array
  17664. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  17665. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  17666. while (i < key.length) {
  17667. split = normalizeLanguage(key[i]).split('-');
  17668. j = split.length;
  17669. next = normalizeLanguage(key[i + 1]);
  17670. next = next ? next.split('-') : null;
  17671. while (j > 0) {
  17672. lang = get(split.slice(0, j).join('-'));
  17673. if (lang) {
  17674. return lang;
  17675. }
  17676. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  17677. //the next array item is better than a shallower substring of this one
  17678. break;
  17679. }
  17680. j--;
  17681. }
  17682. i++;
  17683. }
  17684. return moment.fn._lang;
  17685. }
  17686. /************************************
  17687. Formatting
  17688. ************************************/
  17689. function removeFormattingTokens(input) {
  17690. if (input.match(/\[[\s\S]/)) {
  17691. return input.replace(/^\[|\]$/g, "");
  17692. }
  17693. return input.replace(/\\/g, "");
  17694. }
  17695. function makeFormatFunction(format) {
  17696. var array = format.match(formattingTokens), i, length;
  17697. for (i = 0, length = array.length; i < length; i++) {
  17698. if (formatTokenFunctions[array[i]]) {
  17699. array[i] = formatTokenFunctions[array[i]];
  17700. } else {
  17701. array[i] = removeFormattingTokens(array[i]);
  17702. }
  17703. }
  17704. return function (mom) {
  17705. var output = "";
  17706. for (i = 0; i < length; i++) {
  17707. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  17708. }
  17709. return output;
  17710. };
  17711. }
  17712. // format date using native date object
  17713. function formatMoment(m, format) {
  17714. if (!m.isValid()) {
  17715. return m.lang().invalidDate();
  17716. }
  17717. format = expandFormat(format, m.lang());
  17718. if (!formatFunctions[format]) {
  17719. formatFunctions[format] = makeFormatFunction(format);
  17720. }
  17721. return formatFunctions[format](m);
  17722. }
  17723. function expandFormat(format, lang) {
  17724. var i = 5;
  17725. function replaceLongDateFormatTokens(input) {
  17726. return lang.longDateFormat(input) || input;
  17727. }
  17728. localFormattingTokens.lastIndex = 0;
  17729. while (i >= 0 && localFormattingTokens.test(format)) {
  17730. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  17731. localFormattingTokens.lastIndex = 0;
  17732. i -= 1;
  17733. }
  17734. return format;
  17735. }
  17736. /************************************
  17737. Parsing
  17738. ************************************/
  17739. // get the regex to find the next token
  17740. function getParseRegexForToken(token, config) {
  17741. var a, strict = config._strict;
  17742. switch (token) {
  17743. case 'Q':
  17744. return parseTokenOneDigit;
  17745. case 'DDDD':
  17746. return parseTokenThreeDigits;
  17747. case 'YYYY':
  17748. case 'GGGG':
  17749. case 'gggg':
  17750. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  17751. case 'Y':
  17752. case 'G':
  17753. case 'g':
  17754. return parseTokenSignedNumber;
  17755. case 'YYYYYY':
  17756. case 'YYYYY':
  17757. case 'GGGGG':
  17758. case 'ggggg':
  17759. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  17760. case 'S':
  17761. if (strict) { return parseTokenOneDigit; }
  17762. /* falls through */
  17763. case 'SS':
  17764. if (strict) { return parseTokenTwoDigits; }
  17765. /* falls through */
  17766. case 'SSS':
  17767. if (strict) { return parseTokenThreeDigits; }
  17768. /* falls through */
  17769. case 'DDD':
  17770. return parseTokenOneToThreeDigits;
  17771. case 'MMM':
  17772. case 'MMMM':
  17773. case 'dd':
  17774. case 'ddd':
  17775. case 'dddd':
  17776. return parseTokenWord;
  17777. case 'a':
  17778. case 'A':
  17779. return getLangDefinition(config._l)._meridiemParse;
  17780. case 'X':
  17781. return parseTokenTimestampMs;
  17782. case 'Z':
  17783. case 'ZZ':
  17784. return parseTokenTimezone;
  17785. case 'T':
  17786. return parseTokenT;
  17787. case 'SSSS':
  17788. return parseTokenDigits;
  17789. case 'MM':
  17790. case 'DD':
  17791. case 'YY':
  17792. case 'GG':
  17793. case 'gg':
  17794. case 'HH':
  17795. case 'hh':
  17796. case 'mm':
  17797. case 'ss':
  17798. case 'ww':
  17799. case 'WW':
  17800. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  17801. case 'M':
  17802. case 'D':
  17803. case 'd':
  17804. case 'H':
  17805. case 'h':
  17806. case 'm':
  17807. case 's':
  17808. case 'w':
  17809. case 'W':
  17810. case 'e':
  17811. case 'E':
  17812. return parseTokenOneOrTwoDigits;
  17813. case 'Do':
  17814. return parseTokenOrdinal;
  17815. default :
  17816. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  17817. return a;
  17818. }
  17819. }
  17820. function timezoneMinutesFromString(string) {
  17821. string = string || "";
  17822. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  17823. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  17824. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  17825. minutes = +(parts[1] * 60) + toInt(parts[2]);
  17826. return parts[0] === '+' ? -minutes : minutes;
  17827. }
  17828. // function to convert string input to date
  17829. function addTimeToArrayFromToken(token, input, config) {
  17830. var a, datePartArray = config._a;
  17831. switch (token) {
  17832. // QUARTER
  17833. case 'Q':
  17834. if (input != null) {
  17835. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  17836. }
  17837. break;
  17838. // MONTH
  17839. case 'M' : // fall through to MM
  17840. case 'MM' :
  17841. if (input != null) {
  17842. datePartArray[MONTH] = toInt(input) - 1;
  17843. }
  17844. break;
  17845. case 'MMM' : // fall through to MMMM
  17846. case 'MMMM' :
  17847. a = getLangDefinition(config._l).monthsParse(input);
  17848. // if we didn't find a month name, mark the date as invalid.
  17849. if (a != null) {
  17850. datePartArray[MONTH] = a;
  17851. } else {
  17852. config._pf.invalidMonth = input;
  17853. }
  17854. break;
  17855. // DAY OF MONTH
  17856. case 'D' : // fall through to DD
  17857. case 'DD' :
  17858. if (input != null) {
  17859. datePartArray[DATE] = toInt(input);
  17860. }
  17861. break;
  17862. case 'Do' :
  17863. if (input != null) {
  17864. datePartArray[DATE] = toInt(parseInt(input, 10));
  17865. }
  17866. break;
  17867. // DAY OF YEAR
  17868. case 'DDD' : // fall through to DDDD
  17869. case 'DDDD' :
  17870. if (input != null) {
  17871. config._dayOfYear = toInt(input);
  17872. }
  17873. break;
  17874. // YEAR
  17875. case 'YY' :
  17876. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  17877. break;
  17878. case 'YYYY' :
  17879. case 'YYYYY' :
  17880. case 'YYYYYY' :
  17881. datePartArray[YEAR] = toInt(input);
  17882. break;
  17883. // AM / PM
  17884. case 'a' : // fall through to A
  17885. case 'A' :
  17886. config._isPm = getLangDefinition(config._l).isPM(input);
  17887. break;
  17888. // 24 HOUR
  17889. case 'H' : // fall through to hh
  17890. case 'HH' : // fall through to hh
  17891. case 'h' : // fall through to hh
  17892. case 'hh' :
  17893. datePartArray[HOUR] = toInt(input);
  17894. break;
  17895. // MINUTE
  17896. case 'm' : // fall through to mm
  17897. case 'mm' :
  17898. datePartArray[MINUTE] = toInt(input);
  17899. break;
  17900. // SECOND
  17901. case 's' : // fall through to ss
  17902. case 'ss' :
  17903. datePartArray[SECOND] = toInt(input);
  17904. break;
  17905. // MILLISECOND
  17906. case 'S' :
  17907. case 'SS' :
  17908. case 'SSS' :
  17909. case 'SSSS' :
  17910. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  17911. break;
  17912. // UNIX TIMESTAMP WITH MS
  17913. case 'X':
  17914. config._d = new Date(parseFloat(input) * 1000);
  17915. break;
  17916. // TIMEZONE
  17917. case 'Z' : // fall through to ZZ
  17918. case 'ZZ' :
  17919. config._useUTC = true;
  17920. config._tzm = timezoneMinutesFromString(input);
  17921. break;
  17922. case 'w':
  17923. case 'ww':
  17924. case 'W':
  17925. case 'WW':
  17926. case 'd':
  17927. case 'dd':
  17928. case 'ddd':
  17929. case 'dddd':
  17930. case 'e':
  17931. case 'E':
  17932. token = token.substr(0, 1);
  17933. /* falls through */
  17934. case 'gg':
  17935. case 'gggg':
  17936. case 'GG':
  17937. case 'GGGG':
  17938. case 'GGGGG':
  17939. token = token.substr(0, 2);
  17940. if (input) {
  17941. config._w = config._w || {};
  17942. config._w[token] = input;
  17943. }
  17944. break;
  17945. }
  17946. }
  17947. // convert an array to a date.
  17948. // the array should mirror the parameters below
  17949. // note: all values past the year are optional and will default to the lowest possible value.
  17950. // [year, month, day , hour, minute, second, millisecond]
  17951. function dateFromConfig(config) {
  17952. var i, date, input = [], currentDate,
  17953. yearToUse, fixYear, w, temp, lang, weekday, week;
  17954. if (config._d) {
  17955. return;
  17956. }
  17957. currentDate = currentDateArray(config);
  17958. //compute day of the year from weeks and weekdays
  17959. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  17960. fixYear = function (val) {
  17961. var intVal = parseInt(val, 10);
  17962. return val ?
  17963. (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
  17964. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  17965. };
  17966. w = config._w;
  17967. if (w.GG != null || w.W != null || w.E != null) {
  17968. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  17969. }
  17970. else {
  17971. lang = getLangDefinition(config._l);
  17972. weekday = w.d != null ? parseWeekday(w.d, lang) :
  17973. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  17974. week = parseInt(w.w, 10) || 1;
  17975. //if we're parsing 'd', then the low day numbers may be next week
  17976. if (w.d != null && weekday < lang._week.dow) {
  17977. week++;
  17978. }
  17979. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  17980. }
  17981. config._a[YEAR] = temp.year;
  17982. config._dayOfYear = temp.dayOfYear;
  17983. }
  17984. //if the day of the year is set, figure out what it is
  17985. if (config._dayOfYear) {
  17986. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  17987. if (config._dayOfYear > daysInYear(yearToUse)) {
  17988. config._pf._overflowDayOfYear = true;
  17989. }
  17990. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  17991. config._a[MONTH] = date.getUTCMonth();
  17992. config._a[DATE] = date.getUTCDate();
  17993. }
  17994. // Default to current date.
  17995. // * if no year, month, day of month are given, default to today
  17996. // * if day of month is given, default month and year
  17997. // * if month is given, default only year
  17998. // * if year is given, don't default anything
  17999. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  18000. config._a[i] = input[i] = currentDate[i];
  18001. }
  18002. // Zero out whatever was not defaulted, including time
  18003. for (; i < 7; i++) {
  18004. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  18005. }
  18006. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  18007. input[HOUR] += toInt((config._tzm || 0) / 60);
  18008. input[MINUTE] += toInt((config._tzm || 0) % 60);
  18009. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  18010. }
  18011. function dateFromObject(config) {
  18012. var normalizedInput;
  18013. if (config._d) {
  18014. return;
  18015. }
  18016. normalizedInput = normalizeObjectUnits(config._i);
  18017. config._a = [
  18018. normalizedInput.year,
  18019. normalizedInput.month,
  18020. normalizedInput.day,
  18021. normalizedInput.hour,
  18022. normalizedInput.minute,
  18023. normalizedInput.second,
  18024. normalizedInput.millisecond
  18025. ];
  18026. dateFromConfig(config);
  18027. }
  18028. function currentDateArray(config) {
  18029. var now = new Date();
  18030. if (config._useUTC) {
  18031. return [
  18032. now.getUTCFullYear(),
  18033. now.getUTCMonth(),
  18034. now.getUTCDate()
  18035. ];
  18036. } else {
  18037. return [now.getFullYear(), now.getMonth(), now.getDate()];
  18038. }
  18039. }
  18040. // date from string and format string
  18041. function makeDateFromStringAndFormat(config) {
  18042. config._a = [];
  18043. config._pf.empty = true;
  18044. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  18045. var lang = getLangDefinition(config._l),
  18046. string = '' + config._i,
  18047. i, parsedInput, tokens, token, skipped,
  18048. stringLength = string.length,
  18049. totalParsedInputLength = 0;
  18050. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  18051. for (i = 0; i < tokens.length; i++) {
  18052. token = tokens[i];
  18053. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  18054. if (parsedInput) {
  18055. skipped = string.substr(0, string.indexOf(parsedInput));
  18056. if (skipped.length > 0) {
  18057. config._pf.unusedInput.push(skipped);
  18058. }
  18059. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  18060. totalParsedInputLength += parsedInput.length;
  18061. }
  18062. // don't parse if it's not a known token
  18063. if (formatTokenFunctions[token]) {
  18064. if (parsedInput) {
  18065. config._pf.empty = false;
  18066. }
  18067. else {
  18068. config._pf.unusedTokens.push(token);
  18069. }
  18070. addTimeToArrayFromToken(token, parsedInput, config);
  18071. }
  18072. else if (config._strict && !parsedInput) {
  18073. config._pf.unusedTokens.push(token);
  18074. }
  18075. }
  18076. // add remaining unparsed input length to the string
  18077. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  18078. if (string.length > 0) {
  18079. config._pf.unusedInput.push(string);
  18080. }
  18081. // handle am pm
  18082. if (config._isPm && config._a[HOUR] < 12) {
  18083. config._a[HOUR] += 12;
  18084. }
  18085. // if is 12 am, change hours to 0
  18086. if (config._isPm === false && config._a[HOUR] === 12) {
  18087. config._a[HOUR] = 0;
  18088. }
  18089. dateFromConfig(config);
  18090. checkOverflow(config);
  18091. }
  18092. function unescapeFormat(s) {
  18093. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  18094. return p1 || p2 || p3 || p4;
  18095. });
  18096. }
  18097. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  18098. function regexpEscape(s) {
  18099. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  18100. }
  18101. // date from string and array of format strings
  18102. function makeDateFromStringAndArray(config) {
  18103. var tempConfig,
  18104. bestMoment,
  18105. scoreToBeat,
  18106. i,
  18107. currentScore;
  18108. if (config._f.length === 0) {
  18109. config._pf.invalidFormat = true;
  18110. config._d = new Date(NaN);
  18111. return;
  18112. }
  18113. for (i = 0; i < config._f.length; i++) {
  18114. currentScore = 0;
  18115. tempConfig = extend({}, config);
  18116. tempConfig._pf = defaultParsingFlags();
  18117. tempConfig._f = config._f[i];
  18118. makeDateFromStringAndFormat(tempConfig);
  18119. if (!isValid(tempConfig)) {
  18120. continue;
  18121. }
  18122. // if there is any input that was not parsed add a penalty for that format
  18123. currentScore += tempConfig._pf.charsLeftOver;
  18124. //or tokens
  18125. currentScore += tempConfig._pf.unusedTokens.length * 10;
  18126. tempConfig._pf.score = currentScore;
  18127. if (scoreToBeat == null || currentScore < scoreToBeat) {
  18128. scoreToBeat = currentScore;
  18129. bestMoment = tempConfig;
  18130. }
  18131. }
  18132. extend(config, bestMoment || tempConfig);
  18133. }
  18134. // date from iso format
  18135. function makeDateFromString(config) {
  18136. var i, l,
  18137. string = config._i,
  18138. match = isoRegex.exec(string);
  18139. if (match) {
  18140. config._pf.iso = true;
  18141. for (i = 0, l = isoDates.length; i < l; i++) {
  18142. if (isoDates[i][1].exec(string)) {
  18143. // match[5] should be "T" or undefined
  18144. config._f = isoDates[i][0] + (match[6] || " ");
  18145. break;
  18146. }
  18147. }
  18148. for (i = 0, l = isoTimes.length; i < l; i++) {
  18149. if (isoTimes[i][1].exec(string)) {
  18150. config._f += isoTimes[i][0];
  18151. break;
  18152. }
  18153. }
  18154. if (string.match(parseTokenTimezone)) {
  18155. config._f += "Z";
  18156. }
  18157. makeDateFromStringAndFormat(config);
  18158. }
  18159. else {
  18160. moment.createFromInputFallback(config);
  18161. }
  18162. }
  18163. function makeDateFromInput(config) {
  18164. var input = config._i,
  18165. matched = aspNetJsonRegex.exec(input);
  18166. if (input === undefined) {
  18167. config._d = new Date();
  18168. } else if (matched) {
  18169. config._d = new Date(+matched[1]);
  18170. } else if (typeof input === 'string') {
  18171. makeDateFromString(config);
  18172. } else if (isArray(input)) {
  18173. config._a = input.slice(0);
  18174. dateFromConfig(config);
  18175. } else if (isDate(input)) {
  18176. config._d = new Date(+input);
  18177. } else if (typeof(input) === 'object') {
  18178. dateFromObject(config);
  18179. } else if (typeof(input) === 'number') {
  18180. // from milliseconds
  18181. config._d = new Date(input);
  18182. } else {
  18183. moment.createFromInputFallback(config);
  18184. }
  18185. }
  18186. function makeDate(y, m, d, h, M, s, ms) {
  18187. //can't just apply() to create a date:
  18188. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  18189. var date = new Date(y, m, d, h, M, s, ms);
  18190. //the date constructor doesn't accept years < 1970
  18191. if (y < 1970) {
  18192. date.setFullYear(y);
  18193. }
  18194. return date;
  18195. }
  18196. function makeUTCDate(y) {
  18197. var date = new Date(Date.UTC.apply(null, arguments));
  18198. if (y < 1970) {
  18199. date.setUTCFullYear(y);
  18200. }
  18201. return date;
  18202. }
  18203. function parseWeekday(input, language) {
  18204. if (typeof input === 'string') {
  18205. if (!isNaN(input)) {
  18206. input = parseInt(input, 10);
  18207. }
  18208. else {
  18209. input = language.weekdaysParse(input);
  18210. if (typeof input !== 'number') {
  18211. return null;
  18212. }
  18213. }
  18214. }
  18215. return input;
  18216. }
  18217. /************************************
  18218. Relative Time
  18219. ************************************/
  18220. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  18221. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  18222. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  18223. }
  18224. function relativeTime(milliseconds, withoutSuffix, lang) {
  18225. var seconds = round(Math.abs(milliseconds) / 1000),
  18226. minutes = round(seconds / 60),
  18227. hours = round(minutes / 60),
  18228. days = round(hours / 24),
  18229. years = round(days / 365),
  18230. args = seconds < 45 && ['s', seconds] ||
  18231. minutes === 1 && ['m'] ||
  18232. minutes < 45 && ['mm', minutes] ||
  18233. hours === 1 && ['h'] ||
  18234. hours < 22 && ['hh', hours] ||
  18235. days === 1 && ['d'] ||
  18236. days <= 25 && ['dd', days] ||
  18237. days <= 45 && ['M'] ||
  18238. days < 345 && ['MM', round(days / 30)] ||
  18239. years === 1 && ['y'] || ['yy', years];
  18240. args[2] = withoutSuffix;
  18241. args[3] = milliseconds > 0;
  18242. args[4] = lang;
  18243. return substituteTimeAgo.apply({}, args);
  18244. }
  18245. /************************************
  18246. Week of Year
  18247. ************************************/
  18248. // firstDayOfWeek 0 = sun, 6 = sat
  18249. // the day of the week that starts the week
  18250. // (usually sunday or monday)
  18251. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  18252. // the first week is the week that contains the first
  18253. // of this day of the week
  18254. // (eg. ISO weeks use thursday (4))
  18255. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  18256. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  18257. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  18258. adjustedMoment;
  18259. if (daysToDayOfWeek > end) {
  18260. daysToDayOfWeek -= 7;
  18261. }
  18262. if (daysToDayOfWeek < end - 7) {
  18263. daysToDayOfWeek += 7;
  18264. }
  18265. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  18266. return {
  18267. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  18268. year: adjustedMoment.year()
  18269. };
  18270. }
  18271. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  18272. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  18273. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  18274. weekday = weekday != null ? weekday : firstDayOfWeek;
  18275. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  18276. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  18277. return {
  18278. year: dayOfYear > 0 ? year : year - 1,
  18279. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  18280. };
  18281. }
  18282. /************************************
  18283. Top Level Functions
  18284. ************************************/
  18285. function makeMoment(config) {
  18286. var input = config._i,
  18287. format = config._f;
  18288. if (input === null || (format === undefined && input === '')) {
  18289. return moment.invalid({nullInput: true});
  18290. }
  18291. if (typeof input === 'string') {
  18292. config._i = input = getLangDefinition().preparse(input);
  18293. }
  18294. if (moment.isMoment(input)) {
  18295. config = cloneMoment(input);
  18296. config._d = new Date(+input._d);
  18297. } else if (format) {
  18298. if (isArray(format)) {
  18299. makeDateFromStringAndArray(config);
  18300. } else {
  18301. makeDateFromStringAndFormat(config);
  18302. }
  18303. } else {
  18304. makeDateFromInput(config);
  18305. }
  18306. return new Moment(config);
  18307. }
  18308. moment = function (input, format, lang, strict) {
  18309. var c;
  18310. if (typeof(lang) === "boolean") {
  18311. strict = lang;
  18312. lang = undefined;
  18313. }
  18314. // object construction must be done this way.
  18315. // https://github.com/moment/moment/issues/1423
  18316. c = {};
  18317. c._isAMomentObject = true;
  18318. c._i = input;
  18319. c._f = format;
  18320. c._l = lang;
  18321. c._strict = strict;
  18322. c._isUTC = false;
  18323. c._pf = defaultParsingFlags();
  18324. return makeMoment(c);
  18325. };
  18326. moment.suppressDeprecationWarnings = false;
  18327. moment.createFromInputFallback = deprecate(
  18328. "moment construction falls back to js Date. This is " +
  18329. "discouraged and will be removed in upcoming major " +
  18330. "release. Please refer to " +
  18331. "https://github.com/moment/moment/issues/1407 for more info.",
  18332. function (config) {
  18333. config._d = new Date(config._i);
  18334. });
  18335. // creating with utc
  18336. moment.utc = function (input, format, lang, strict) {
  18337. var c;
  18338. if (typeof(lang) === "boolean") {
  18339. strict = lang;
  18340. lang = undefined;
  18341. }
  18342. // object construction must be done this way.
  18343. // https://github.com/moment/moment/issues/1423
  18344. c = {};
  18345. c._isAMomentObject = true;
  18346. c._useUTC = true;
  18347. c._isUTC = true;
  18348. c._l = lang;
  18349. c._i = input;
  18350. c._f = format;
  18351. c._strict = strict;
  18352. c._pf = defaultParsingFlags();
  18353. return makeMoment(c).utc();
  18354. };
  18355. // creating with unix timestamp (in seconds)
  18356. moment.unix = function (input) {
  18357. return moment(input * 1000);
  18358. };
  18359. // duration
  18360. moment.duration = function (input, key) {
  18361. var duration = input,
  18362. // matching against regexp is expensive, do it on demand
  18363. match = null,
  18364. sign,
  18365. ret,
  18366. parseIso;
  18367. if (moment.isDuration(input)) {
  18368. duration = {
  18369. ms: input._milliseconds,
  18370. d: input._days,
  18371. M: input._months
  18372. };
  18373. } else if (typeof input === 'number') {
  18374. duration = {};
  18375. if (key) {
  18376. duration[key] = input;
  18377. } else {
  18378. duration.milliseconds = input;
  18379. }
  18380. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  18381. sign = (match[1] === "-") ? -1 : 1;
  18382. duration = {
  18383. y: 0,
  18384. d: toInt(match[DATE]) * sign,
  18385. h: toInt(match[HOUR]) * sign,
  18386. m: toInt(match[MINUTE]) * sign,
  18387. s: toInt(match[SECOND]) * sign,
  18388. ms: toInt(match[MILLISECOND]) * sign
  18389. };
  18390. } else if (!!(match = isoDurationRegex.exec(input))) {
  18391. sign = (match[1] === "-") ? -1 : 1;
  18392. parseIso = function (inp) {
  18393. // We'd normally use ~~inp for this, but unfortunately it also
  18394. // converts floats to ints.
  18395. // inp may be undefined, so careful calling replace on it.
  18396. var res = inp && parseFloat(inp.replace(',', '.'));
  18397. // apply sign while we're at it
  18398. return (isNaN(res) ? 0 : res) * sign;
  18399. };
  18400. duration = {
  18401. y: parseIso(match[2]),
  18402. M: parseIso(match[3]),
  18403. d: parseIso(match[4]),
  18404. h: parseIso(match[5]),
  18405. m: parseIso(match[6]),
  18406. s: parseIso(match[7]),
  18407. w: parseIso(match[8])
  18408. };
  18409. }
  18410. ret = new Duration(duration);
  18411. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  18412. ret._lang = input._lang;
  18413. }
  18414. return ret;
  18415. };
  18416. // version number
  18417. moment.version = VERSION;
  18418. // default format
  18419. moment.defaultFormat = isoFormat;
  18420. // Plugins that add properties should also add the key here (null value),
  18421. // so we can properly clone ourselves.
  18422. moment.momentProperties = momentProperties;
  18423. // This function will be called whenever a moment is mutated.
  18424. // It is intended to keep the offset in sync with the timezone.
  18425. moment.updateOffset = function () {};
  18426. // This function will load languages and then set the global language. If
  18427. // no arguments are passed in, it will simply return the current global
  18428. // language key.
  18429. moment.lang = function (key, values) {
  18430. var r;
  18431. if (!key) {
  18432. return moment.fn._lang._abbr;
  18433. }
  18434. if (values) {
  18435. loadLang(normalizeLanguage(key), values);
  18436. } else if (values === null) {
  18437. unloadLang(key);
  18438. key = 'en';
  18439. } else if (!languages[key]) {
  18440. getLangDefinition(key);
  18441. }
  18442. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  18443. return r._abbr;
  18444. };
  18445. // returns language data
  18446. moment.langData = function (key) {
  18447. if (key && key._lang && key._lang._abbr) {
  18448. key = key._lang._abbr;
  18449. }
  18450. return getLangDefinition(key);
  18451. };
  18452. // compare moment object
  18453. moment.isMoment = function (obj) {
  18454. return obj instanceof Moment ||
  18455. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  18456. };
  18457. // for typechecking Duration objects
  18458. moment.isDuration = function (obj) {
  18459. return obj instanceof Duration;
  18460. };
  18461. for (i = lists.length - 1; i >= 0; --i) {
  18462. makeList(lists[i]);
  18463. }
  18464. moment.normalizeUnits = function (units) {
  18465. return normalizeUnits(units);
  18466. };
  18467. moment.invalid = function (flags) {
  18468. var m = moment.utc(NaN);
  18469. if (flags != null) {
  18470. extend(m._pf, flags);
  18471. }
  18472. else {
  18473. m._pf.userInvalidated = true;
  18474. }
  18475. return m;
  18476. };
  18477. moment.parseZone = function () {
  18478. return moment.apply(null, arguments).parseZone();
  18479. };
  18480. moment.parseTwoDigitYear = function (input) {
  18481. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  18482. };
  18483. /************************************
  18484. Moment Prototype
  18485. ************************************/
  18486. extend(moment.fn = Moment.prototype, {
  18487. clone : function () {
  18488. return moment(this);
  18489. },
  18490. valueOf : function () {
  18491. return +this._d + ((this._offset || 0) * 60000);
  18492. },
  18493. unix : function () {
  18494. return Math.floor(+this / 1000);
  18495. },
  18496. toString : function () {
  18497. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  18498. },
  18499. toDate : function () {
  18500. return this._offset ? new Date(+this) : this._d;
  18501. },
  18502. toISOString : function () {
  18503. var m = moment(this).utc();
  18504. if (0 < m.year() && m.year() <= 9999) {
  18505. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18506. } else {
  18507. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  18508. }
  18509. },
  18510. toArray : function () {
  18511. var m = this;
  18512. return [
  18513. m.year(),
  18514. m.month(),
  18515. m.date(),
  18516. m.hours(),
  18517. m.minutes(),
  18518. m.seconds(),
  18519. m.milliseconds()
  18520. ];
  18521. },
  18522. isValid : function () {
  18523. return isValid(this);
  18524. },
  18525. isDSTShifted : function () {
  18526. if (this._a) {
  18527. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  18528. }
  18529. return false;
  18530. },
  18531. parsingFlags : function () {
  18532. return extend({}, this._pf);
  18533. },
  18534. invalidAt: function () {
  18535. return this._pf.overflow;
  18536. },
  18537. utc : function () {
  18538. return this.zone(0);
  18539. },
  18540. local : function () {
  18541. this.zone(0);
  18542. this._isUTC = false;
  18543. return this;
  18544. },
  18545. format : function (inputString) {
  18546. var output = formatMoment(this, inputString || moment.defaultFormat);
  18547. return this.lang().postformat(output);
  18548. },
  18549. add : function (input, val) {
  18550. var dur;
  18551. // switch args to support add('s', 1) and add(1, 's')
  18552. if (typeof input === 'string') {
  18553. dur = moment.duration(+val, input);
  18554. } else {
  18555. dur = moment.duration(input, val);
  18556. }
  18557. addOrSubtractDurationFromMoment(this, dur, 1);
  18558. return this;
  18559. },
  18560. subtract : function (input, val) {
  18561. var dur;
  18562. // switch args to support subtract('s', 1) and subtract(1, 's')
  18563. if (typeof input === 'string') {
  18564. dur = moment.duration(+val, input);
  18565. } else {
  18566. dur = moment.duration(input, val);
  18567. }
  18568. addOrSubtractDurationFromMoment(this, dur, -1);
  18569. return this;
  18570. },
  18571. diff : function (input, units, asFloat) {
  18572. var that = makeAs(input, this),
  18573. zoneDiff = (this.zone() - that.zone()) * 6e4,
  18574. diff, output;
  18575. units = normalizeUnits(units);
  18576. if (units === 'year' || units === 'month') {
  18577. // average number of days in the months in the given dates
  18578. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  18579. // difference in months
  18580. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  18581. // adjust by taking difference in days, average number of days
  18582. // and dst in the given months.
  18583. output += ((this - moment(this).startOf('month')) -
  18584. (that - moment(that).startOf('month'))) / diff;
  18585. // same as above but with zones, to negate all dst
  18586. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  18587. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  18588. if (units === 'year') {
  18589. output = output / 12;
  18590. }
  18591. } else {
  18592. diff = (this - that);
  18593. output = units === 'second' ? diff / 1e3 : // 1000
  18594. units === 'minute' ? diff / 6e4 : // 1000 * 60
  18595. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  18596. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  18597. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  18598. diff;
  18599. }
  18600. return asFloat ? output : absRound(output);
  18601. },
  18602. from : function (time, withoutSuffix) {
  18603. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  18604. },
  18605. fromNow : function (withoutSuffix) {
  18606. return this.from(moment(), withoutSuffix);
  18607. },
  18608. calendar : function () {
  18609. // We want to compare the start of today, vs this.
  18610. // Getting start-of-today depends on whether we're zone'd or not.
  18611. var sod = makeAs(moment(), this).startOf('day'),
  18612. diff = this.diff(sod, 'days', true),
  18613. format = diff < -6 ? 'sameElse' :
  18614. diff < -1 ? 'lastWeek' :
  18615. diff < 0 ? 'lastDay' :
  18616. diff < 1 ? 'sameDay' :
  18617. diff < 2 ? 'nextDay' :
  18618. diff < 7 ? 'nextWeek' : 'sameElse';
  18619. return this.format(this.lang().calendar(format, this));
  18620. },
  18621. isLeapYear : function () {
  18622. return isLeapYear(this.year());
  18623. },
  18624. isDST : function () {
  18625. return (this.zone() < this.clone().month(0).zone() ||
  18626. this.zone() < this.clone().month(5).zone());
  18627. },
  18628. day : function (input) {
  18629. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  18630. if (input != null) {
  18631. input = parseWeekday(input, this.lang());
  18632. return this.add({ d : input - day });
  18633. } else {
  18634. return day;
  18635. }
  18636. },
  18637. month : makeAccessor('Month', true),
  18638. startOf: function (units) {
  18639. units = normalizeUnits(units);
  18640. // the following switch intentionally omits break keywords
  18641. // to utilize falling through the cases.
  18642. switch (units) {
  18643. case 'year':
  18644. this.month(0);
  18645. /* falls through */
  18646. case 'quarter':
  18647. case 'month':
  18648. this.date(1);
  18649. /* falls through */
  18650. case 'week':
  18651. case 'isoWeek':
  18652. case 'day':
  18653. this.hours(0);
  18654. /* falls through */
  18655. case 'hour':
  18656. this.minutes(0);
  18657. /* falls through */
  18658. case 'minute':
  18659. this.seconds(0);
  18660. /* falls through */
  18661. case 'second':
  18662. this.milliseconds(0);
  18663. /* falls through */
  18664. }
  18665. // weeks are a special case
  18666. if (units === 'week') {
  18667. this.weekday(0);
  18668. } else if (units === 'isoWeek') {
  18669. this.isoWeekday(1);
  18670. }
  18671. // quarters are also special
  18672. if (units === 'quarter') {
  18673. this.month(Math.floor(this.month() / 3) * 3);
  18674. }
  18675. return this;
  18676. },
  18677. endOf: function (units) {
  18678. units = normalizeUnits(units);
  18679. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  18680. },
  18681. isAfter: function (input, units) {
  18682. units = typeof units !== 'undefined' ? units : 'millisecond';
  18683. return +this.clone().startOf(units) > +moment(input).startOf(units);
  18684. },
  18685. isBefore: function (input, units) {
  18686. units = typeof units !== 'undefined' ? units : 'millisecond';
  18687. return +this.clone().startOf(units) < +moment(input).startOf(units);
  18688. },
  18689. isSame: function (input, units) {
  18690. units = units || 'ms';
  18691. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  18692. },
  18693. min: function (other) {
  18694. other = moment.apply(null, arguments);
  18695. return other < this ? this : other;
  18696. },
  18697. max: function (other) {
  18698. other = moment.apply(null, arguments);
  18699. return other > this ? this : other;
  18700. },
  18701. // keepTime = true means only change the timezone, without affecting
  18702. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  18703. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  18704. // adjust the time as needed, to be valid.
  18705. //
  18706. // Keeping the time actually adds/subtracts (one hour)
  18707. // from the actual represented time. That is why we call updateOffset
  18708. // a second time. In case it wants us to change the offset again
  18709. // _changeInProgress == true case, then we have to adjust, because
  18710. // there is no such time in the given timezone.
  18711. zone : function (input, keepTime) {
  18712. var offset = this._offset || 0;
  18713. if (input != null) {
  18714. if (typeof input === "string") {
  18715. input = timezoneMinutesFromString(input);
  18716. }
  18717. if (Math.abs(input) < 16) {
  18718. input = input * 60;
  18719. }
  18720. this._offset = input;
  18721. this._isUTC = true;
  18722. if (offset !== input) {
  18723. if (!keepTime || this._changeInProgress) {
  18724. addOrSubtractDurationFromMoment(this,
  18725. moment.duration(offset - input, 'm'), 1, false);
  18726. } else if (!this._changeInProgress) {
  18727. this._changeInProgress = true;
  18728. moment.updateOffset(this, true);
  18729. this._changeInProgress = null;
  18730. }
  18731. }
  18732. } else {
  18733. return this._isUTC ? offset : this._d.getTimezoneOffset();
  18734. }
  18735. return this;
  18736. },
  18737. zoneAbbr : function () {
  18738. return this._isUTC ? "UTC" : "";
  18739. },
  18740. zoneName : function () {
  18741. return this._isUTC ? "Coordinated Universal Time" : "";
  18742. },
  18743. parseZone : function () {
  18744. if (this._tzm) {
  18745. this.zone(this._tzm);
  18746. } else if (typeof this._i === 'string') {
  18747. this.zone(this._i);
  18748. }
  18749. return this;
  18750. },
  18751. hasAlignedHourOffset : function (input) {
  18752. if (!input) {
  18753. input = 0;
  18754. }
  18755. else {
  18756. input = moment(input).zone();
  18757. }
  18758. return (this.zone() - input) % 60 === 0;
  18759. },
  18760. daysInMonth : function () {
  18761. return daysInMonth(this.year(), this.month());
  18762. },
  18763. dayOfYear : function (input) {
  18764. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  18765. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  18766. },
  18767. quarter : function (input) {
  18768. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  18769. },
  18770. weekYear : function (input) {
  18771. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  18772. return input == null ? year : this.add("y", (input - year));
  18773. },
  18774. isoWeekYear : function (input) {
  18775. var year = weekOfYear(this, 1, 4).year;
  18776. return input == null ? year : this.add("y", (input - year));
  18777. },
  18778. week : function (input) {
  18779. var week = this.lang().week(this);
  18780. return input == null ? week : this.add("d", (input - week) * 7);
  18781. },
  18782. isoWeek : function (input) {
  18783. var week = weekOfYear(this, 1, 4).week;
  18784. return input == null ? week : this.add("d", (input - week) * 7);
  18785. },
  18786. weekday : function (input) {
  18787. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  18788. return input == null ? weekday : this.add("d", input - weekday);
  18789. },
  18790. isoWeekday : function (input) {
  18791. // behaves the same as moment#day except
  18792. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  18793. // as a setter, sunday should belong to the previous week.
  18794. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  18795. },
  18796. isoWeeksInYear : function () {
  18797. return weeksInYear(this.year(), 1, 4);
  18798. },
  18799. weeksInYear : function () {
  18800. var weekInfo = this._lang._week;
  18801. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  18802. },
  18803. get : function (units) {
  18804. units = normalizeUnits(units);
  18805. return this[units]();
  18806. },
  18807. set : function (units, value) {
  18808. units = normalizeUnits(units);
  18809. if (typeof this[units] === 'function') {
  18810. this[units](value);
  18811. }
  18812. return this;
  18813. },
  18814. // If passed a language key, it will set the language for this
  18815. // instance. Otherwise, it will return the language configuration
  18816. // variables for this instance.
  18817. lang : function (key) {
  18818. if (key === undefined) {
  18819. return this._lang;
  18820. } else {
  18821. this._lang = getLangDefinition(key);
  18822. return this;
  18823. }
  18824. }
  18825. });
  18826. function rawMonthSetter(mom, value) {
  18827. var dayOfMonth;
  18828. // TODO: Move this out of here!
  18829. if (typeof value === 'string') {
  18830. value = mom.lang().monthsParse(value);
  18831. // TODO: Another silent failure?
  18832. if (typeof value !== 'number') {
  18833. return mom;
  18834. }
  18835. }
  18836. dayOfMonth = Math.min(mom.date(),
  18837. daysInMonth(mom.year(), value));
  18838. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  18839. return mom;
  18840. }
  18841. function rawGetter(mom, unit) {
  18842. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  18843. }
  18844. function rawSetter(mom, unit, value) {
  18845. if (unit === 'Month') {
  18846. return rawMonthSetter(mom, value);
  18847. } else {
  18848. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  18849. }
  18850. }
  18851. function makeAccessor(unit, keepTime) {
  18852. return function (value) {
  18853. if (value != null) {
  18854. rawSetter(this, unit, value);
  18855. moment.updateOffset(this, keepTime);
  18856. return this;
  18857. } else {
  18858. return rawGetter(this, unit);
  18859. }
  18860. };
  18861. }
  18862. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  18863. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  18864. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  18865. // Setting the hour should keep the time, because the user explicitly
  18866. // specified which hour he wants. So trying to maintain the same hour (in
  18867. // a new timezone) makes sense. Adding/subtracting hours does not follow
  18868. // this rule.
  18869. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  18870. // moment.fn.month is defined separately
  18871. moment.fn.date = makeAccessor('Date', true);
  18872. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  18873. moment.fn.year = makeAccessor('FullYear', true);
  18874. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  18875. // add plural methods
  18876. moment.fn.days = moment.fn.day;
  18877. moment.fn.months = moment.fn.month;
  18878. moment.fn.weeks = moment.fn.week;
  18879. moment.fn.isoWeeks = moment.fn.isoWeek;
  18880. moment.fn.quarters = moment.fn.quarter;
  18881. // add aliased format methods
  18882. moment.fn.toJSON = moment.fn.toISOString;
  18883. /************************************
  18884. Duration Prototype
  18885. ************************************/
  18886. extend(moment.duration.fn = Duration.prototype, {
  18887. _bubble : function () {
  18888. var milliseconds = this._milliseconds,
  18889. days = this._days,
  18890. months = this._months,
  18891. data = this._data,
  18892. seconds, minutes, hours, years;
  18893. // The following code bubbles up values, see the tests for
  18894. // examples of what that means.
  18895. data.milliseconds = milliseconds % 1000;
  18896. seconds = absRound(milliseconds / 1000);
  18897. data.seconds = seconds % 60;
  18898. minutes = absRound(seconds / 60);
  18899. data.minutes = minutes % 60;
  18900. hours = absRound(minutes / 60);
  18901. data.hours = hours % 24;
  18902. days += absRound(hours / 24);
  18903. data.days = days % 30;
  18904. months += absRound(days / 30);
  18905. data.months = months % 12;
  18906. years = absRound(months / 12);
  18907. data.years = years;
  18908. },
  18909. weeks : function () {
  18910. return absRound(this.days() / 7);
  18911. },
  18912. valueOf : function () {
  18913. return this._milliseconds +
  18914. this._days * 864e5 +
  18915. (this._months % 12) * 2592e6 +
  18916. toInt(this._months / 12) * 31536e6;
  18917. },
  18918. humanize : function (withSuffix) {
  18919. var difference = +this,
  18920. output = relativeTime(difference, !withSuffix, this.lang());
  18921. if (withSuffix) {
  18922. output = this.lang().pastFuture(difference, output);
  18923. }
  18924. return this.lang().postformat(output);
  18925. },
  18926. add : function (input, val) {
  18927. // supports only 2.0-style add(1, 's') or add(moment)
  18928. var dur = moment.duration(input, val);
  18929. this._milliseconds += dur._milliseconds;
  18930. this._days += dur._days;
  18931. this._months += dur._months;
  18932. this._bubble();
  18933. return this;
  18934. },
  18935. subtract : function (input, val) {
  18936. var dur = moment.duration(input, val);
  18937. this._milliseconds -= dur._milliseconds;
  18938. this._days -= dur._days;
  18939. this._months -= dur._months;
  18940. this._bubble();
  18941. return this;
  18942. },
  18943. get : function (units) {
  18944. units = normalizeUnits(units);
  18945. return this[units.toLowerCase() + 's']();
  18946. },
  18947. as : function (units) {
  18948. units = normalizeUnits(units);
  18949. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  18950. },
  18951. lang : moment.fn.lang,
  18952. toIsoString : function () {
  18953. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  18954. var years = Math.abs(this.years()),
  18955. months = Math.abs(this.months()),
  18956. days = Math.abs(this.days()),
  18957. hours = Math.abs(this.hours()),
  18958. minutes = Math.abs(this.minutes()),
  18959. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  18960. if (!this.asSeconds()) {
  18961. // this is the same as C#'s (Noda) and python (isodate)...
  18962. // but not other JS (goog.date)
  18963. return 'P0D';
  18964. }
  18965. return (this.asSeconds() < 0 ? '-' : '') +
  18966. 'P' +
  18967. (years ? years + 'Y' : '') +
  18968. (months ? months + 'M' : '') +
  18969. (days ? days + 'D' : '') +
  18970. ((hours || minutes || seconds) ? 'T' : '') +
  18971. (hours ? hours + 'H' : '') +
  18972. (minutes ? minutes + 'M' : '') +
  18973. (seconds ? seconds + 'S' : '');
  18974. }
  18975. });
  18976. function makeDurationGetter(name) {
  18977. moment.duration.fn[name] = function () {
  18978. return this._data[name];
  18979. };
  18980. }
  18981. function makeDurationAsGetter(name, factor) {
  18982. moment.duration.fn['as' + name] = function () {
  18983. return +this / factor;
  18984. };
  18985. }
  18986. for (i in unitMillisecondFactors) {
  18987. if (unitMillisecondFactors.hasOwnProperty(i)) {
  18988. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  18989. makeDurationGetter(i.toLowerCase());
  18990. }
  18991. }
  18992. makeDurationAsGetter('Weeks', 6048e5);
  18993. moment.duration.fn.asMonths = function () {
  18994. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  18995. };
  18996. /************************************
  18997. Default Lang
  18998. ************************************/
  18999. // Set default language, other languages will inherit from English.
  19000. moment.lang('en', {
  19001. ordinal : function (number) {
  19002. var b = number % 10,
  19003. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  19004. (b === 1) ? 'st' :
  19005. (b === 2) ? 'nd' :
  19006. (b === 3) ? 'rd' : 'th';
  19007. return number + output;
  19008. }
  19009. });
  19010. /* EMBED_LANGUAGES */
  19011. /************************************
  19012. Exposing Moment
  19013. ************************************/
  19014. function makeGlobal(shouldDeprecate) {
  19015. /*global ender:false */
  19016. if (typeof ender !== 'undefined') {
  19017. return;
  19018. }
  19019. oldGlobalMoment = globalScope.moment;
  19020. if (shouldDeprecate) {
  19021. globalScope.moment = deprecate(
  19022. "Accessing Moment through the global scope is " +
  19023. "deprecated, and will be removed in an upcoming " +
  19024. "release.",
  19025. moment);
  19026. } else {
  19027. globalScope.moment = moment;
  19028. }
  19029. }
  19030. // CommonJS module is defined
  19031. if (hasModule) {
  19032. module.exports = moment;
  19033. } else if (typeof define === "function" && define.amd) {
  19034. define("moment", function (require, exports, module) {
  19035. if (module.config && module.config() && module.config().noGlobal === true) {
  19036. // release the global variable
  19037. globalScope.moment = oldGlobalMoment;
  19038. }
  19039. return moment;
  19040. });
  19041. makeGlobal(true);
  19042. } else {
  19043. makeGlobal();
  19044. }
  19045. }).call(this);
  19046. },{}],5:[function(require,module,exports){
  19047. /**
  19048. * Copyright 2012 Craig Campbell
  19049. *
  19050. * Licensed under the Apache License, Version 2.0 (the "License");
  19051. * you may not use this file except in compliance with the License.
  19052. * You may obtain a copy of the License at
  19053. *
  19054. * http://www.apache.org/licenses/LICENSE-2.0
  19055. *
  19056. * Unless required by applicable law or agreed to in writing, software
  19057. * distributed under the License is distributed on an "AS IS" BASIS,
  19058. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  19059. * See the License for the specific language governing permissions and
  19060. * limitations under the License.
  19061. *
  19062. * Mousetrap is a simple keyboard shortcut library for Javascript with
  19063. * no external dependencies
  19064. *
  19065. * @version 1.1.2
  19066. * @url craig.is/killing/mice
  19067. */
  19068. /**
  19069. * mapping of special keycodes to their corresponding keys
  19070. *
  19071. * everything in this dictionary cannot use keypress events
  19072. * so it has to be here to map to the correct keycodes for
  19073. * keyup/keydown events
  19074. *
  19075. * @type {Object}
  19076. */
  19077. var _MAP = {
  19078. 8: 'backspace',
  19079. 9: 'tab',
  19080. 13: 'enter',
  19081. 16: 'shift',
  19082. 17: 'ctrl',
  19083. 18: 'alt',
  19084. 20: 'capslock',
  19085. 27: 'esc',
  19086. 32: 'space',
  19087. 33: 'pageup',
  19088. 34: 'pagedown',
  19089. 35: 'end',
  19090. 36: 'home',
  19091. 37: 'left',
  19092. 38: 'up',
  19093. 39: 'right',
  19094. 40: 'down',
  19095. 45: 'ins',
  19096. 46: 'del',
  19097. 91: 'meta',
  19098. 93: 'meta',
  19099. 224: 'meta'
  19100. },
  19101. /**
  19102. * mapping for special characters so they can support
  19103. *
  19104. * this dictionary is only used incase you want to bind a
  19105. * keyup or keydown event to one of these keys
  19106. *
  19107. * @type {Object}
  19108. */
  19109. _KEYCODE_MAP = {
  19110. 106: '*',
  19111. 107: '+',
  19112. 109: '-',
  19113. 110: '.',
  19114. 111 : '/',
  19115. 186: ';',
  19116. 187: '=',
  19117. 188: ',',
  19118. 189: '-',
  19119. 190: '.',
  19120. 191: '/',
  19121. 192: '`',
  19122. 219: '[',
  19123. 220: '\\',
  19124. 221: ']',
  19125. 222: '\''
  19126. },
  19127. /**
  19128. * this is a mapping of keys that require shift on a US keypad
  19129. * back to the non shift equivelents
  19130. *
  19131. * this is so you can use keyup events with these keys
  19132. *
  19133. * note that this will only work reliably on US keyboards
  19134. *
  19135. * @type {Object}
  19136. */
  19137. _SHIFT_MAP = {
  19138. '~': '`',
  19139. '!': '1',
  19140. '@': '2',
  19141. '#': '3',
  19142. '$': '4',
  19143. '%': '5',
  19144. '^': '6',
  19145. '&': '7',
  19146. '*': '8',
  19147. '(': '9',
  19148. ')': '0',
  19149. '_': '-',
  19150. '+': '=',
  19151. ':': ';',
  19152. '\"': '\'',
  19153. '<': ',',
  19154. '>': '.',
  19155. '?': '/',
  19156. '|': '\\'
  19157. },
  19158. /**
  19159. * this is a list of special strings you can use to map
  19160. * to modifier keys when you specify your keyboard shortcuts
  19161. *
  19162. * @type {Object}
  19163. */
  19164. _SPECIAL_ALIASES = {
  19165. 'option': 'alt',
  19166. 'command': 'meta',
  19167. 'return': 'enter',
  19168. 'escape': 'esc'
  19169. },
  19170. /**
  19171. * variable to store the flipped version of _MAP from above
  19172. * needed to check if we should use keypress or not when no action
  19173. * is specified
  19174. *
  19175. * @type {Object|undefined}
  19176. */
  19177. _REVERSE_MAP,
  19178. /**
  19179. * a list of all the callbacks setup via Mousetrap.bind()
  19180. *
  19181. * @type {Object}
  19182. */
  19183. _callbacks = {},
  19184. /**
  19185. * direct map of string combinations to callbacks used for trigger()
  19186. *
  19187. * @type {Object}
  19188. */
  19189. _direct_map = {},
  19190. /**
  19191. * keeps track of what level each sequence is at since multiple
  19192. * sequences can start out with the same sequence
  19193. *
  19194. * @type {Object}
  19195. */
  19196. _sequence_levels = {},
  19197. /**
  19198. * variable to store the setTimeout call
  19199. *
  19200. * @type {null|number}
  19201. */
  19202. _reset_timer,
  19203. /**
  19204. * temporary state where we will ignore the next keyup
  19205. *
  19206. * @type {boolean|string}
  19207. */
  19208. _ignore_next_keyup = false,
  19209. /**
  19210. * are we currently inside of a sequence?
  19211. * type of action ("keyup" or "keydown" or "keypress") or false
  19212. *
  19213. * @type {boolean|string}
  19214. */
  19215. _inside_sequence = false;
  19216. /**
  19217. * loop through the f keys, f1 to f19 and add them to the map
  19218. * programatically
  19219. */
  19220. for (var i = 1; i < 20; ++i) {
  19221. _MAP[111 + i] = 'f' + i;
  19222. }
  19223. /**
  19224. * loop through to map numbers on the numeric keypad
  19225. */
  19226. for (i = 0; i <= 9; ++i) {
  19227. _MAP[i + 96] = i;
  19228. }
  19229. /**
  19230. * cross browser add event method
  19231. *
  19232. * @param {Element|HTMLDocument} object
  19233. * @param {string} type
  19234. * @param {Function} callback
  19235. * @returns void
  19236. */
  19237. function _addEvent(object, type, callback) {
  19238. if (object.addEventListener) {
  19239. return object.addEventListener(type, callback, false);
  19240. }
  19241. object.attachEvent('on' + type, callback);
  19242. }
  19243. /**
  19244. * takes the event and returns the key character
  19245. *
  19246. * @param {Event} e
  19247. * @return {string}
  19248. */
  19249. function _characterFromEvent(e) {
  19250. // for keypress events we should return the character as is
  19251. if (e.type == 'keypress') {
  19252. return String.fromCharCode(e.which);
  19253. }
  19254. // for non keypress events the special maps are needed
  19255. if (_MAP[e.which]) {
  19256. return _MAP[e.which];
  19257. }
  19258. if (_KEYCODE_MAP[e.which]) {
  19259. return _KEYCODE_MAP[e.which];
  19260. }
  19261. // if it is not in the special map
  19262. return String.fromCharCode(e.which).toLowerCase();
  19263. }
  19264. /**
  19265. * should we stop this event before firing off callbacks
  19266. *
  19267. * @param {Event} e
  19268. * @return {boolean}
  19269. */
  19270. function _stop(e) {
  19271. var element = e.target || e.srcElement,
  19272. tag_name = element.tagName;
  19273. // if the element has the class "mousetrap" then no need to stop
  19274. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  19275. return false;
  19276. }
  19277. // stop for input, select, and textarea
  19278. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  19279. }
  19280. /**
  19281. * checks if two arrays are equal
  19282. *
  19283. * @param {Array} modifiers1
  19284. * @param {Array} modifiers2
  19285. * @returns {boolean}
  19286. */
  19287. function _modifiersMatch(modifiers1, modifiers2) {
  19288. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  19289. }
  19290. /**
  19291. * resets all sequence counters except for the ones passed in
  19292. *
  19293. * @param {Object} do_not_reset
  19294. * @returns void
  19295. */
  19296. function _resetSequences(do_not_reset) {
  19297. do_not_reset = do_not_reset || {};
  19298. var active_sequences = false,
  19299. key;
  19300. for (key in _sequence_levels) {
  19301. if (do_not_reset[key]) {
  19302. active_sequences = true;
  19303. continue;
  19304. }
  19305. _sequence_levels[key] = 0;
  19306. }
  19307. if (!active_sequences) {
  19308. _inside_sequence = false;
  19309. }
  19310. }
  19311. /**
  19312. * finds all callbacks that match based on the keycode, modifiers,
  19313. * and action
  19314. *
  19315. * @param {string} character
  19316. * @param {Array} modifiers
  19317. * @param {string} action
  19318. * @param {boolean=} remove - should we remove any matches
  19319. * @param {string=} combination
  19320. * @returns {Array}
  19321. */
  19322. function _getMatches(character, modifiers, action, remove, combination) {
  19323. var i,
  19324. callback,
  19325. matches = [];
  19326. // if there are no events related to this keycode
  19327. if (!_callbacks[character]) {
  19328. return [];
  19329. }
  19330. // if a modifier key is coming up on its own we should allow it
  19331. if (action == 'keyup' && _isModifier(character)) {
  19332. modifiers = [character];
  19333. }
  19334. // loop through all callbacks for the key that was pressed
  19335. // and see if any of them match
  19336. for (i = 0; i < _callbacks[character].length; ++i) {
  19337. callback = _callbacks[character][i];
  19338. // if this is a sequence but it is not at the right level
  19339. // then move onto the next match
  19340. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  19341. continue;
  19342. }
  19343. // if the action we are looking for doesn't match the action we got
  19344. // then we should keep going
  19345. if (action != callback.action) {
  19346. continue;
  19347. }
  19348. // if this is a keypress event that means that we need to only
  19349. // look at the character, otherwise check the modifiers as
  19350. // well
  19351. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  19352. // remove is used so if you change your mind and call bind a
  19353. // second time with a new function the first one is overwritten
  19354. if (remove && callback.combo == combination) {
  19355. _callbacks[character].splice(i, 1);
  19356. }
  19357. matches.push(callback);
  19358. }
  19359. }
  19360. return matches;
  19361. }
  19362. /**
  19363. * takes a key event and figures out what the modifiers are
  19364. *
  19365. * @param {Event} e
  19366. * @returns {Array}
  19367. */
  19368. function _eventModifiers(e) {
  19369. var modifiers = [];
  19370. if (e.shiftKey) {
  19371. modifiers.push('shift');
  19372. }
  19373. if (e.altKey) {
  19374. modifiers.push('alt');
  19375. }
  19376. if (e.ctrlKey) {
  19377. modifiers.push('ctrl');
  19378. }
  19379. if (e.metaKey) {
  19380. modifiers.push('meta');
  19381. }
  19382. return modifiers;
  19383. }
  19384. /**
  19385. * actually calls the callback function
  19386. *
  19387. * if your callback function returns false this will use the jquery
  19388. * convention - prevent default and stop propogation on the event
  19389. *
  19390. * @param {Function} callback
  19391. * @param {Event} e
  19392. * @returns void
  19393. */
  19394. function _fireCallback(callback, e) {
  19395. if (callback(e) === false) {
  19396. if (e.preventDefault) {
  19397. e.preventDefault();
  19398. }
  19399. if (e.stopPropagation) {
  19400. e.stopPropagation();
  19401. }
  19402. e.returnValue = false;
  19403. e.cancelBubble = true;
  19404. }
  19405. }
  19406. /**
  19407. * handles a character key event
  19408. *
  19409. * @param {string} character
  19410. * @param {Event} e
  19411. * @returns void
  19412. */
  19413. function _handleCharacter(character, e) {
  19414. // if this event should not happen stop here
  19415. if (_stop(e)) {
  19416. return;
  19417. }
  19418. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  19419. i,
  19420. do_not_reset = {},
  19421. processed_sequence_callback = false;
  19422. // loop through matching callbacks for this key event
  19423. for (i = 0; i < callbacks.length; ++i) {
  19424. // fire for all sequence callbacks
  19425. // this is because if for example you have multiple sequences
  19426. // bound such as "g i" and "g t" they both need to fire the
  19427. // callback for matching g cause otherwise you can only ever
  19428. // match the first one
  19429. if (callbacks[i].seq) {
  19430. processed_sequence_callback = true;
  19431. // keep a list of which sequences were matches for later
  19432. do_not_reset[callbacks[i].seq] = 1;
  19433. _fireCallback(callbacks[i].callback, e);
  19434. continue;
  19435. }
  19436. // if there were no sequence matches but we are still here
  19437. // that means this is a regular match so we should fire that
  19438. if (!processed_sequence_callback && !_inside_sequence) {
  19439. _fireCallback(callbacks[i].callback, e);
  19440. }
  19441. }
  19442. // if you are inside of a sequence and the key you are pressing
  19443. // is not a modifier key then we should reset all sequences
  19444. // that were not matched by this key event
  19445. if (e.type == _inside_sequence && !_isModifier(character)) {
  19446. _resetSequences(do_not_reset);
  19447. }
  19448. }
  19449. /**
  19450. * handles a keydown event
  19451. *
  19452. * @param {Event} e
  19453. * @returns void
  19454. */
  19455. function _handleKey(e) {
  19456. // normalize e.which for key events
  19457. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  19458. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  19459. var character = _characterFromEvent(e);
  19460. // no character found then stop
  19461. if (!character) {
  19462. return;
  19463. }
  19464. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  19465. _ignore_next_keyup = false;
  19466. return;
  19467. }
  19468. _handleCharacter(character, e);
  19469. }
  19470. /**
  19471. * determines if the keycode specified is a modifier key or not
  19472. *
  19473. * @param {string} key
  19474. * @returns {boolean}
  19475. */
  19476. function _isModifier(key) {
  19477. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  19478. }
  19479. /**
  19480. * called to set a 1 second timeout on the specified sequence
  19481. *
  19482. * this is so after each key press in the sequence you have 1 second
  19483. * to press the next key before you have to start over
  19484. *
  19485. * @returns void
  19486. */
  19487. function _resetSequenceTimer() {
  19488. clearTimeout(_reset_timer);
  19489. _reset_timer = setTimeout(_resetSequences, 1000);
  19490. }
  19491. /**
  19492. * reverses the map lookup so that we can look for specific keys
  19493. * to see what can and can't use keypress
  19494. *
  19495. * @return {Object}
  19496. */
  19497. function _getReverseMap() {
  19498. if (!_REVERSE_MAP) {
  19499. _REVERSE_MAP = {};
  19500. for (var key in _MAP) {
  19501. // pull out the numeric keypad from here cause keypress should
  19502. // be able to detect the keys from the character
  19503. if (key > 95 && key < 112) {
  19504. continue;
  19505. }
  19506. if (_MAP.hasOwnProperty(key)) {
  19507. _REVERSE_MAP[_MAP[key]] = key;
  19508. }
  19509. }
  19510. }
  19511. return _REVERSE_MAP;
  19512. }
  19513. /**
  19514. * picks the best action based on the key combination
  19515. *
  19516. * @param {string} key - character for key
  19517. * @param {Array} modifiers
  19518. * @param {string=} action passed in
  19519. */
  19520. function _pickBestAction(key, modifiers, action) {
  19521. // if no action was picked in we should try to pick the one
  19522. // that we think would work best for this key
  19523. if (!action) {
  19524. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  19525. }
  19526. // modifier keys don't work as expected with keypress,
  19527. // switch to keydown
  19528. if (action == 'keypress' && modifiers.length) {
  19529. action = 'keydown';
  19530. }
  19531. return action;
  19532. }
  19533. /**
  19534. * binds a key sequence to an event
  19535. *
  19536. * @param {string} combo - combo specified in bind call
  19537. * @param {Array} keys
  19538. * @param {Function} callback
  19539. * @param {string=} action
  19540. * @returns void
  19541. */
  19542. function _bindSequence(combo, keys, callback, action) {
  19543. // start off by adding a sequence level record for this combination
  19544. // and setting the level to 0
  19545. _sequence_levels[combo] = 0;
  19546. // if there is no action pick the best one for the first key
  19547. // in the sequence
  19548. if (!action) {
  19549. action = _pickBestAction(keys[0], []);
  19550. }
  19551. /**
  19552. * callback to increase the sequence level for this sequence and reset
  19553. * all other sequences that were active
  19554. *
  19555. * @param {Event} e
  19556. * @returns void
  19557. */
  19558. var _increaseSequence = function(e) {
  19559. _inside_sequence = action;
  19560. ++_sequence_levels[combo];
  19561. _resetSequenceTimer();
  19562. },
  19563. /**
  19564. * wraps the specified callback inside of another function in order
  19565. * to reset all sequence counters as soon as this sequence is done
  19566. *
  19567. * @param {Event} e
  19568. * @returns void
  19569. */
  19570. _callbackAndReset = function(e) {
  19571. _fireCallback(callback, e);
  19572. // we should ignore the next key up if the action is key down
  19573. // or keypress. this is so if you finish a sequence and
  19574. // release the key the final key will not trigger a keyup
  19575. if (action !== 'keyup') {
  19576. _ignore_next_keyup = _characterFromEvent(e);
  19577. }
  19578. // weird race condition if a sequence ends with the key
  19579. // another sequence begins with
  19580. setTimeout(_resetSequences, 10);
  19581. },
  19582. i;
  19583. // loop through keys one at a time and bind the appropriate callback
  19584. // function. for any key leading up to the final one it should
  19585. // increase the sequence. after the final, it should reset all sequences
  19586. for (i = 0; i < keys.length; ++i) {
  19587. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  19588. }
  19589. }
  19590. /**
  19591. * binds a single keyboard combination
  19592. *
  19593. * @param {string} combination
  19594. * @param {Function} callback
  19595. * @param {string=} action
  19596. * @param {string=} sequence_name - name of sequence if part of sequence
  19597. * @param {number=} level - what part of the sequence the command is
  19598. * @returns void
  19599. */
  19600. function _bindSingle(combination, callback, action, sequence_name, level) {
  19601. // make sure multiple spaces in a row become a single space
  19602. combination = combination.replace(/\s+/g, ' ');
  19603. var sequence = combination.split(' '),
  19604. i,
  19605. key,
  19606. keys,
  19607. modifiers = [];
  19608. // if this pattern is a sequence of keys then run through this method
  19609. // to reprocess each pattern one key at a time
  19610. if (sequence.length > 1) {
  19611. return _bindSequence(combination, sequence, callback, action);
  19612. }
  19613. // take the keys from this pattern and figure out what the actual
  19614. // pattern is all about
  19615. keys = combination === '+' ? ['+'] : combination.split('+');
  19616. for (i = 0; i < keys.length; ++i) {
  19617. key = keys[i];
  19618. // normalize key names
  19619. if (_SPECIAL_ALIASES[key]) {
  19620. key = _SPECIAL_ALIASES[key];
  19621. }
  19622. // if this is not a keypress event then we should
  19623. // be smart about using shift keys
  19624. // this will only work for US keyboards however
  19625. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  19626. key = _SHIFT_MAP[key];
  19627. modifiers.push('shift');
  19628. }
  19629. // if this key is a modifier then add it to the list of modifiers
  19630. if (_isModifier(key)) {
  19631. modifiers.push(key);
  19632. }
  19633. }
  19634. // depending on what the key combination is
  19635. // we will try to pick the best event for it
  19636. action = _pickBestAction(key, modifiers, action);
  19637. // make sure to initialize array if this is the first time
  19638. // a callback is added for this key
  19639. if (!_callbacks[key]) {
  19640. _callbacks[key] = [];
  19641. }
  19642. // remove an existing match if there is one
  19643. _getMatches(key, modifiers, action, !sequence_name, combination);
  19644. // add this call back to the array
  19645. // if it is a sequence put it at the beginning
  19646. // if not put it at the end
  19647. //
  19648. // this is important because the way these are processed expects
  19649. // the sequence ones to come first
  19650. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  19651. callback: callback,
  19652. modifiers: modifiers,
  19653. action: action,
  19654. seq: sequence_name,
  19655. level: level,
  19656. combo: combination
  19657. });
  19658. }
  19659. /**
  19660. * binds multiple combinations to the same callback
  19661. *
  19662. * @param {Array} combinations
  19663. * @param {Function} callback
  19664. * @param {string|undefined} action
  19665. * @returns void
  19666. */
  19667. function _bindMultiple(combinations, callback, action) {
  19668. for (var i = 0; i < combinations.length; ++i) {
  19669. _bindSingle(combinations[i], callback, action);
  19670. }
  19671. }
  19672. // start!
  19673. _addEvent(document, 'keypress', _handleKey);
  19674. _addEvent(document, 'keydown', _handleKey);
  19675. _addEvent(document, 'keyup', _handleKey);
  19676. var mousetrap = {
  19677. /**
  19678. * binds an event to mousetrap
  19679. *
  19680. * can be a single key, a combination of keys separated with +,
  19681. * a comma separated list of keys, an array of keys, or
  19682. * a sequence of keys separated by spaces
  19683. *
  19684. * be sure to list the modifier keys first to make sure that the
  19685. * correct key ends up getting bound (the last key in the pattern)
  19686. *
  19687. * @param {string|Array} keys
  19688. * @param {Function} callback
  19689. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  19690. * @returns void
  19691. */
  19692. bind: function(keys, callback, action) {
  19693. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  19694. _direct_map[keys + ':' + action] = callback;
  19695. return this;
  19696. },
  19697. /**
  19698. * unbinds an event to mousetrap
  19699. *
  19700. * the unbinding sets the callback function of the specified key combo
  19701. * to an empty function and deletes the corresponding key in the
  19702. * _direct_map dict.
  19703. *
  19704. * the keycombo+action has to be exactly the same as
  19705. * it was defined in the bind method
  19706. *
  19707. * TODO: actually remove this from the _callbacks dictionary instead
  19708. * of binding an empty function
  19709. *
  19710. * @param {string|Array} keys
  19711. * @param {string} action
  19712. * @returns void
  19713. */
  19714. unbind: function(keys, action) {
  19715. if (_direct_map[keys + ':' + action]) {
  19716. delete _direct_map[keys + ':' + action];
  19717. this.bind(keys, function() {}, action);
  19718. }
  19719. return this;
  19720. },
  19721. /**
  19722. * triggers an event that has already been bound
  19723. *
  19724. * @param {string} keys
  19725. * @param {string=} action
  19726. * @returns void
  19727. */
  19728. trigger: function(keys, action) {
  19729. _direct_map[keys + ':' + action]();
  19730. return this;
  19731. },
  19732. /**
  19733. * resets the library back to its initial state. this is useful
  19734. * if you want to clear out the current keyboard shortcuts and bind
  19735. * new ones - for example if you switch to another page
  19736. *
  19737. * @returns void
  19738. */
  19739. reset: function() {
  19740. _callbacks = {};
  19741. _direct_map = {};
  19742. return this;
  19743. }
  19744. };
  19745. module.exports = mousetrap;
  19746. },{}]},{},[1])
  19747. (1)
  19748. });