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.

26301 lines
776 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 1.1.0
  8. * @date 2014-06-10
  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. * Deep extend an object a with the properties of object b
  354. * @param {Object} a
  355. * @param {Object} b
  356. * @returns {Object}
  357. */
  358. util.deepExtend = function deepExtend (a, b) {
  359. // TODO: add support for Arrays to deepExtend
  360. if (Array.isArray(b)) {
  361. throw new TypeError('Arrays are not supported by deepExtend');
  362. }
  363. for (var prop in b) {
  364. if (b.hasOwnProperty(prop)) {
  365. if (b[prop] && b[prop].constructor === Object) {
  366. if (a[prop] === undefined) {
  367. a[prop] = {};
  368. }
  369. if (a[prop].constructor === Object) {
  370. deepExtend(a[prop], b[prop]);
  371. }
  372. else {
  373. a[prop] = b[prop];
  374. }
  375. } else if (Array.isArray(b[prop])) {
  376. throw new TypeError('Arrays are not supported by deepExtend');
  377. } else {
  378. a[prop] = b[prop];
  379. }
  380. }
  381. }
  382. return a;
  383. };
  384. /**
  385. * Test whether all elements in two arrays are equal.
  386. * @param {Array} a
  387. * @param {Array} b
  388. * @return {boolean} Returns true if both arrays have the same length and same
  389. * elements.
  390. */
  391. util.equalArray = function (a, b) {
  392. if (a.length != b.length) return false;
  393. for (var i = 0, len = a.length; i < len; i++) {
  394. if (a[i] != b[i]) return false;
  395. }
  396. return true;
  397. };
  398. /**
  399. * Convert an object to another type
  400. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  401. * @param {String | undefined} type Name of the type. Available types:
  402. * 'Boolean', 'Number', 'String',
  403. * 'Date', 'Moment', ISODate', 'ASPDate'.
  404. * @return {*} object
  405. * @throws Error
  406. */
  407. util.convert = function convert(object, type) {
  408. var match;
  409. if (object === undefined) {
  410. return undefined;
  411. }
  412. if (object === null) {
  413. return null;
  414. }
  415. if (!type) {
  416. return object;
  417. }
  418. if (!(typeof type === 'string') && !(type instanceof String)) {
  419. throw new Error('Type must be a string');
  420. }
  421. //noinspection FallthroughInSwitchStatementJS
  422. switch (type) {
  423. case 'boolean':
  424. case 'Boolean':
  425. return Boolean(object);
  426. case 'number':
  427. case 'Number':
  428. return Number(object.valueOf());
  429. case 'string':
  430. case 'String':
  431. return String(object);
  432. case 'Date':
  433. if (util.isNumber(object)) {
  434. return new Date(object);
  435. }
  436. if (object instanceof Date) {
  437. return new Date(object.valueOf());
  438. }
  439. else if (moment.isMoment(object)) {
  440. return new Date(object.valueOf());
  441. }
  442. if (util.isString(object)) {
  443. match = ASPDateRegex.exec(object);
  444. if (match) {
  445. // object is an ASP date
  446. return new Date(Number(match[1])); // parse number
  447. }
  448. else {
  449. return moment(object).toDate(); // parse string
  450. }
  451. }
  452. else {
  453. throw new Error(
  454. 'Cannot convert object of type ' + util.getType(object) +
  455. ' to type Date');
  456. }
  457. case 'Moment':
  458. if (util.isNumber(object)) {
  459. return moment(object);
  460. }
  461. if (object instanceof Date) {
  462. return moment(object.valueOf());
  463. }
  464. else if (moment.isMoment(object)) {
  465. return moment(object);
  466. }
  467. if (util.isString(object)) {
  468. match = ASPDateRegex.exec(object);
  469. if (match) {
  470. // object is an ASP date
  471. return moment(Number(match[1])); // parse number
  472. }
  473. else {
  474. return moment(object); // parse string
  475. }
  476. }
  477. else {
  478. throw new Error(
  479. 'Cannot convert object of type ' + util.getType(object) +
  480. ' to type Date');
  481. }
  482. case 'ISODate':
  483. if (util.isNumber(object)) {
  484. return new Date(object);
  485. }
  486. else if (object instanceof Date) {
  487. return object.toISOString();
  488. }
  489. else if (moment.isMoment(object)) {
  490. return object.toDate().toISOString();
  491. }
  492. else if (util.isString(object)) {
  493. match = ASPDateRegex.exec(object);
  494. if (match) {
  495. // object is an ASP date
  496. return new Date(Number(match[1])).toISOString(); // parse number
  497. }
  498. else {
  499. return new Date(object).toISOString(); // parse string
  500. }
  501. }
  502. else {
  503. throw new Error(
  504. 'Cannot convert object of type ' + util.getType(object) +
  505. ' to type ISODate');
  506. }
  507. case 'ASPDate':
  508. if (util.isNumber(object)) {
  509. return '/Date(' + object + ')/';
  510. }
  511. else if (object instanceof Date) {
  512. return '/Date(' + object.valueOf() + ')/';
  513. }
  514. else if (util.isString(object)) {
  515. match = ASPDateRegex.exec(object);
  516. var value;
  517. if (match) {
  518. // object is an ASP date
  519. value = new Date(Number(match[1])).valueOf(); // parse number
  520. }
  521. else {
  522. value = new Date(object).valueOf(); // parse string
  523. }
  524. return '/Date(' + value + ')/';
  525. }
  526. else {
  527. throw new Error(
  528. 'Cannot convert object of type ' + util.getType(object) +
  529. ' to type ASPDate');
  530. }
  531. default:
  532. throw new Error('Cannot convert object of type ' + util.getType(object) +
  533. ' to type "' + type + '"');
  534. }
  535. };
  536. // parse ASP.Net Date pattern,
  537. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  538. // code from http://momentjs.com/
  539. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  540. /**
  541. * Get the type of an object, for example util.getType([]) returns 'Array'
  542. * @param {*} object
  543. * @return {String} type
  544. */
  545. util.getType = function getType(object) {
  546. var type = typeof object;
  547. if (type == 'object') {
  548. if (object == null) {
  549. return 'null';
  550. }
  551. if (object instanceof Boolean) {
  552. return 'Boolean';
  553. }
  554. if (object instanceof Number) {
  555. return 'Number';
  556. }
  557. if (object instanceof String) {
  558. return 'String';
  559. }
  560. if (object instanceof Array) {
  561. return 'Array';
  562. }
  563. if (object instanceof Date) {
  564. return 'Date';
  565. }
  566. return 'Object';
  567. }
  568. else if (type == 'number') {
  569. return 'Number';
  570. }
  571. else if (type == 'boolean') {
  572. return 'Boolean';
  573. }
  574. else if (type == 'string') {
  575. return 'String';
  576. }
  577. return type;
  578. };
  579. /**
  580. * Retrieve the absolute left value of a DOM element
  581. * @param {Element} elem A dom element, for example a div
  582. * @return {number} left The absolute left position of this element
  583. * in the browser page.
  584. */
  585. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  586. var doc = document.documentElement;
  587. var body = document.body;
  588. var left = elem.offsetLeft;
  589. var e = elem.offsetParent;
  590. while (e != null && e != body && e != doc) {
  591. left += e.offsetLeft;
  592. left -= e.scrollLeft;
  593. e = e.offsetParent;
  594. }
  595. return left;
  596. };
  597. /**
  598. * Retrieve the absolute top value of a DOM element
  599. * @param {Element} elem A dom element, for example a div
  600. * @return {number} top The absolute top position of this element
  601. * in the browser page.
  602. */
  603. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  604. var doc = document.documentElement;
  605. var body = document.body;
  606. var top = elem.offsetTop;
  607. var e = elem.offsetParent;
  608. while (e != null && e != body && e != doc) {
  609. top += e.offsetTop;
  610. top -= e.scrollTop;
  611. e = e.offsetParent;
  612. }
  613. return top;
  614. };
  615. /**
  616. * Get the absolute, vertical mouse position from an event.
  617. * @param {Event} event
  618. * @return {Number} pageY
  619. */
  620. util.getPageY = function getPageY (event) {
  621. if ('pageY' in event) {
  622. return event.pageY;
  623. }
  624. else {
  625. var clientY;
  626. if (('targetTouches' in event) && event.targetTouches.length) {
  627. clientY = event.targetTouches[0].clientY;
  628. }
  629. else {
  630. clientY = event.clientY;
  631. }
  632. var doc = document.documentElement;
  633. var body = document.body;
  634. return clientY +
  635. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  636. ( doc && doc.clientTop || body && body.clientTop || 0 );
  637. }
  638. };
  639. /**
  640. * Get the absolute, horizontal mouse position from an event.
  641. * @param {Event} event
  642. * @return {Number} pageX
  643. */
  644. util.getPageX = function getPageX (event) {
  645. if ('pageY' in event) {
  646. return event.pageX;
  647. }
  648. else {
  649. var clientX;
  650. if (('targetTouches' in event) && event.targetTouches.length) {
  651. clientX = event.targetTouches[0].clientX;
  652. }
  653. else {
  654. clientX = event.clientX;
  655. }
  656. var doc = document.documentElement;
  657. var body = document.body;
  658. return clientX +
  659. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  660. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  661. }
  662. };
  663. /**
  664. * add a className to the given elements style
  665. * @param {Element} elem
  666. * @param {String} className
  667. */
  668. util.addClassName = function addClassName(elem, className) {
  669. var classes = elem.className.split(' ');
  670. if (classes.indexOf(className) == -1) {
  671. classes.push(className); // add the class to the array
  672. elem.className = classes.join(' ');
  673. }
  674. };
  675. /**
  676. * add a className to the given elements style
  677. * @param {Element} elem
  678. * @param {String} className
  679. */
  680. util.removeClassName = function removeClassname(elem, className) {
  681. var classes = elem.className.split(' ');
  682. var index = classes.indexOf(className);
  683. if (index != -1) {
  684. classes.splice(index, 1); // remove the class from the array
  685. elem.className = classes.join(' ');
  686. }
  687. };
  688. /**
  689. * For each method for both arrays and objects.
  690. * In case of an array, the built-in Array.forEach() is applied.
  691. * In case of an Object, the method loops over all properties of the object.
  692. * @param {Object | Array} object An Object or Array
  693. * @param {function} callback Callback method, called for each item in
  694. * the object or array with three parameters:
  695. * callback(value, index, object)
  696. */
  697. util.forEach = function forEach (object, callback) {
  698. var i,
  699. len;
  700. if (object instanceof Array) {
  701. // array
  702. for (i = 0, len = object.length; i < len; i++) {
  703. callback(object[i], i, object);
  704. }
  705. }
  706. else {
  707. // object
  708. for (i in object) {
  709. if (object.hasOwnProperty(i)) {
  710. callback(object[i], i, object);
  711. }
  712. }
  713. }
  714. };
  715. /**
  716. * Convert an object into an array: all objects properties are put into the
  717. * array. The resulting array is unordered.
  718. * @param {Object} object
  719. * @param {Array} array
  720. */
  721. util.toArray = function toArray(object) {
  722. var array = [];
  723. for (var prop in object) {
  724. if (object.hasOwnProperty(prop)) array.push(object[prop]);
  725. }
  726. return array;
  727. }
  728. /**
  729. * Update a property in an object
  730. * @param {Object} object
  731. * @param {String} key
  732. * @param {*} value
  733. * @return {Boolean} changed
  734. */
  735. util.updateProperty = function updateProperty (object, key, value) {
  736. if (object[key] !== value) {
  737. object[key] = value;
  738. return true;
  739. }
  740. else {
  741. return false;
  742. }
  743. };
  744. /**
  745. * Add and event listener. Works for all browsers
  746. * @param {Element} element An html element
  747. * @param {string} action The action, for example "click",
  748. * without the prefix "on"
  749. * @param {function} listener The callback function to be executed
  750. * @param {boolean} [useCapture]
  751. */
  752. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  753. if (element.addEventListener) {
  754. if (useCapture === undefined)
  755. useCapture = false;
  756. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  757. action = "DOMMouseScroll"; // For Firefox
  758. }
  759. element.addEventListener(action, listener, useCapture);
  760. } else {
  761. element.attachEvent("on" + action, listener); // IE browsers
  762. }
  763. };
  764. /**
  765. * Remove an event listener from an element
  766. * @param {Element} element An html dom element
  767. * @param {string} action The name of the event, for example "mousedown"
  768. * @param {function} listener The listener function
  769. * @param {boolean} [useCapture]
  770. */
  771. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  772. if (element.removeEventListener) {
  773. // non-IE browsers
  774. if (useCapture === undefined)
  775. useCapture = false;
  776. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  777. action = "DOMMouseScroll"; // For Firefox
  778. }
  779. element.removeEventListener(action, listener, useCapture);
  780. } else {
  781. // IE browsers
  782. element.detachEvent("on" + action, listener);
  783. }
  784. };
  785. /**
  786. * Get HTML element which is the target of the event
  787. * @param {Event} event
  788. * @return {Element} target element
  789. */
  790. util.getTarget = function getTarget(event) {
  791. // code from http://www.quirksmode.org/js/events_properties.html
  792. if (!event) {
  793. event = window.event;
  794. }
  795. var target;
  796. if (event.target) {
  797. target = event.target;
  798. }
  799. else if (event.srcElement) {
  800. target = event.srcElement;
  801. }
  802. if (target.nodeType != undefined && target.nodeType == 3) {
  803. // defeat Safari bug
  804. target = target.parentNode;
  805. }
  806. return target;
  807. };
  808. /**
  809. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  810. * @param {Element} element
  811. * @param {Event} event
  812. */
  813. util.fakeGesture = function fakeGesture (element, event) {
  814. var eventType = null;
  815. // for hammer.js 1.0.5
  816. var gesture = Hammer.event.collectEventData(this, eventType, event);
  817. // for hammer.js 1.0.6
  818. //var touches = Hammer.event.getTouchList(event, eventType);
  819. // var gesture = Hammer.event.collectEventData(this, eventType, touches, event);
  820. // on IE in standards mode, no touches are recognized by hammer.js,
  821. // resulting in NaN values for center.pageX and center.pageY
  822. if (isNaN(gesture.center.pageX)) {
  823. gesture.center.pageX = event.pageX;
  824. }
  825. if (isNaN(gesture.center.pageY)) {
  826. gesture.center.pageY = event.pageY;
  827. }
  828. return gesture;
  829. };
  830. util.option = {};
  831. /**
  832. * Convert a value into a boolean
  833. * @param {Boolean | function | undefined} value
  834. * @param {Boolean} [defaultValue]
  835. * @returns {Boolean} bool
  836. */
  837. util.option.asBoolean = function (value, defaultValue) {
  838. if (typeof value == 'function') {
  839. value = value();
  840. }
  841. if (value != null) {
  842. return (value != false);
  843. }
  844. return defaultValue || null;
  845. };
  846. /**
  847. * Convert a value into a number
  848. * @param {Boolean | function | undefined} value
  849. * @param {Number} [defaultValue]
  850. * @returns {Number} number
  851. */
  852. util.option.asNumber = function (value, defaultValue) {
  853. if (typeof value == 'function') {
  854. value = value();
  855. }
  856. if (value != null) {
  857. return Number(value) || defaultValue || null;
  858. }
  859. return defaultValue || null;
  860. };
  861. /**
  862. * Convert a value into a string
  863. * @param {String | function | undefined} value
  864. * @param {String} [defaultValue]
  865. * @returns {String} str
  866. */
  867. util.option.asString = function (value, defaultValue) {
  868. if (typeof value == 'function') {
  869. value = value();
  870. }
  871. if (value != null) {
  872. return String(value);
  873. }
  874. return defaultValue || null;
  875. };
  876. /**
  877. * Convert a size or location into a string with pixels or a percentage
  878. * @param {String | Number | function | undefined} value
  879. * @param {String} [defaultValue]
  880. * @returns {String} size
  881. */
  882. util.option.asSize = function (value, defaultValue) {
  883. if (typeof value == 'function') {
  884. value = value();
  885. }
  886. if (util.isString(value)) {
  887. return value;
  888. }
  889. else if (util.isNumber(value)) {
  890. return value + 'px';
  891. }
  892. else {
  893. return defaultValue || null;
  894. }
  895. };
  896. /**
  897. * Convert a value into a DOM element
  898. * @param {HTMLElement | function | undefined} value
  899. * @param {HTMLElement} [defaultValue]
  900. * @returns {HTMLElement | null} dom
  901. */
  902. util.option.asElement = function (value, defaultValue) {
  903. if (typeof value == 'function') {
  904. value = value();
  905. }
  906. return value || defaultValue || null;
  907. };
  908. util.GiveDec = function GiveDec(Hex) {
  909. var Value;
  910. if (Hex == "A")
  911. Value = 10;
  912. else if (Hex == "B")
  913. Value = 11;
  914. else if (Hex == "C")
  915. Value = 12;
  916. else if (Hex == "D")
  917. Value = 13;
  918. else if (Hex == "E")
  919. Value = 14;
  920. else if (Hex == "F")
  921. Value = 15;
  922. else
  923. Value = eval(Hex);
  924. return Value;
  925. };
  926. util.GiveHex = function GiveHex(Dec) {
  927. var Value;
  928. if(Dec == 10)
  929. Value = "A";
  930. else if (Dec == 11)
  931. Value = "B";
  932. else if (Dec == 12)
  933. Value = "C";
  934. else if (Dec == 13)
  935. Value = "D";
  936. else if (Dec == 14)
  937. Value = "E";
  938. else if (Dec == 15)
  939. Value = "F";
  940. else
  941. Value = "" + Dec;
  942. return Value;
  943. };
  944. /**
  945. * Parse a color property into an object with border, background, and
  946. * highlight colors
  947. * @param {Object | String} color
  948. * @return {Object} colorObject
  949. */
  950. util.parseColor = function(color) {
  951. var c;
  952. if (util.isString(color)) {
  953. if (util.isValidHex(color)) {
  954. var hsv = util.hexToHSV(color);
  955. var lighterColorHSV = {h:hsv.h,s:hsv.s * 0.45,v:Math.min(1,hsv.v * 1.05)};
  956. var darkerColorHSV = {h:hsv.h,s:Math.min(1,hsv.v * 1.25),v:hsv.v*0.6};
  957. var darkerColorHex = util.HSVToHex(darkerColorHSV.h ,darkerColorHSV.h ,darkerColorHSV.v);
  958. var lighterColorHex = util.HSVToHex(lighterColorHSV.h,lighterColorHSV.s,lighterColorHSV.v);
  959. c = {
  960. background: color,
  961. border:darkerColorHex,
  962. highlight: {
  963. background:lighterColorHex,
  964. border:darkerColorHex
  965. },
  966. hover: {
  967. background:lighterColorHex,
  968. border:darkerColorHex
  969. }
  970. };
  971. }
  972. else {
  973. c = {
  974. background:color,
  975. border:color,
  976. highlight: {
  977. background:color,
  978. border:color
  979. },
  980. hover: {
  981. background:color,
  982. border:color
  983. }
  984. };
  985. }
  986. }
  987. else {
  988. c = {};
  989. c.background = color.background || 'white';
  990. c.border = color.border || c.background;
  991. if (util.isString(color.highlight)) {
  992. c.highlight = {
  993. border: color.highlight,
  994. background: color.highlight
  995. }
  996. }
  997. else {
  998. c.highlight = {};
  999. c.highlight.background = color.highlight && color.highlight.background || c.background;
  1000. c.highlight.border = color.highlight && color.highlight.border || c.border;
  1001. }
  1002. if (util.isString(color.hover)) {
  1003. c.hover = {
  1004. border: color.hover,
  1005. background: color.hover
  1006. }
  1007. }
  1008. else {
  1009. c.hover = {};
  1010. c.hover.background = color.hover && color.hover.background || c.background;
  1011. c.hover.border = color.hover && color.hover.border || c.border;
  1012. }
  1013. }
  1014. return c;
  1015. };
  1016. /**
  1017. * http://www.yellowpipe.com/yis/tools/hex-to-rgb/color-converter.php
  1018. *
  1019. * @param {String} hex
  1020. * @returns {{r: *, g: *, b: *}}
  1021. */
  1022. util.hexToRGB = function hexToRGB(hex) {
  1023. hex = hex.replace("#","").toUpperCase();
  1024. var a = util.GiveDec(hex.substring(0, 1));
  1025. var b = util.GiveDec(hex.substring(1, 2));
  1026. var c = util.GiveDec(hex.substring(2, 3));
  1027. var d = util.GiveDec(hex.substring(3, 4));
  1028. var e = util.GiveDec(hex.substring(4, 5));
  1029. var f = util.GiveDec(hex.substring(5, 6));
  1030. var r = (a * 16) + b;
  1031. var g = (c * 16) + d;
  1032. var b = (e * 16) + f;
  1033. return {r:r,g:g,b:b};
  1034. };
  1035. util.RGBToHex = function RGBToHex(red,green,blue) {
  1036. var a = util.GiveHex(Math.floor(red / 16));
  1037. var b = util.GiveHex(red % 16);
  1038. var c = util.GiveHex(Math.floor(green / 16));
  1039. var d = util.GiveHex(green % 16);
  1040. var e = util.GiveHex(Math.floor(blue / 16));
  1041. var f = util.GiveHex(blue % 16);
  1042. var hex = a + b + c + d + e + f;
  1043. return "#" + hex;
  1044. };
  1045. /**
  1046. * http://www.javascripter.net/faq/rgb2hsv.htm
  1047. *
  1048. * @param red
  1049. * @param green
  1050. * @param blue
  1051. * @returns {*}
  1052. * @constructor
  1053. */
  1054. util.RGBToHSV = function RGBToHSV (red,green,blue) {
  1055. red=red/255; green=green/255; blue=blue/255;
  1056. var minRGB = Math.min(red,Math.min(green,blue));
  1057. var maxRGB = Math.max(red,Math.max(green,blue));
  1058. // Black-gray-white
  1059. if (minRGB == maxRGB) {
  1060. return {h:0,s:0,v:minRGB};
  1061. }
  1062. // Colors other than black-gray-white:
  1063. var d = (red==minRGB) ? green-blue : ((blue==minRGB) ? red-green : blue-red);
  1064. var h = (red==minRGB) ? 3 : ((blue==minRGB) ? 1 : 5);
  1065. var hue = 60*(h - d/(maxRGB - minRGB))/360;
  1066. var saturation = (maxRGB - minRGB)/maxRGB;
  1067. var value = maxRGB;
  1068. return {h:hue,s:saturation,v:value};
  1069. };
  1070. /**
  1071. * https://gist.github.com/mjijackson/5311256
  1072. * @param hue
  1073. * @param saturation
  1074. * @param value
  1075. * @returns {{r: number, g: number, b: number}}
  1076. * @constructor
  1077. */
  1078. util.HSVToRGB = function HSVToRGB(h, s, v) {
  1079. var r, g, b;
  1080. var i = Math.floor(h * 6);
  1081. var f = h * 6 - i;
  1082. var p = v * (1 - s);
  1083. var q = v * (1 - f * s);
  1084. var t = v * (1 - (1 - f) * s);
  1085. switch (i % 6) {
  1086. case 0: r = v, g = t, b = p; break;
  1087. case 1: r = q, g = v, b = p; break;
  1088. case 2: r = p, g = v, b = t; break;
  1089. case 3: r = p, g = q, b = v; break;
  1090. case 4: r = t, g = p, b = v; break;
  1091. case 5: r = v, g = p, b = q; break;
  1092. }
  1093. return {r:Math.floor(r * 255), g:Math.floor(g * 255), b:Math.floor(b * 255) };
  1094. };
  1095. util.HSVToHex = function HSVToHex(h, s, v) {
  1096. var rgb = util.HSVToRGB(h, s, v);
  1097. return util.RGBToHex(rgb.r, rgb.g, rgb.b);
  1098. };
  1099. util.hexToHSV = function hexToHSV(hex) {
  1100. var rgb = util.hexToRGB(hex);
  1101. return util.RGBToHSV(rgb.r, rgb.g, rgb.b);
  1102. };
  1103. util.isValidHex = function isValidHex(hex) {
  1104. var isOk = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(hex);
  1105. return isOk;
  1106. };
  1107. util.copyObject = function copyObject(objectFrom, objectTo) {
  1108. for (var i in objectFrom) {
  1109. if (objectFrom.hasOwnProperty(i)) {
  1110. if (typeof objectFrom[i] == "object") {
  1111. objectTo[i] = {};
  1112. util.copyObject(objectFrom[i], objectTo[i]);
  1113. }
  1114. else {
  1115. objectTo[i] = objectFrom[i];
  1116. }
  1117. }
  1118. }
  1119. };
  1120. /**
  1121. * DataSet
  1122. *
  1123. * Usage:
  1124. * var dataSet = new DataSet({
  1125. * fieldId: '_id',
  1126. * convert: {
  1127. * // ...
  1128. * }
  1129. * });
  1130. *
  1131. * dataSet.add(item);
  1132. * dataSet.add(data);
  1133. * dataSet.update(item);
  1134. * dataSet.update(data);
  1135. * dataSet.remove(id);
  1136. * dataSet.remove(ids);
  1137. * var data = dataSet.get();
  1138. * var data = dataSet.get(id);
  1139. * var data = dataSet.get(ids);
  1140. * var data = dataSet.get(ids, options, data);
  1141. * dataSet.clear();
  1142. *
  1143. * A data set can:
  1144. * - add/remove/update data
  1145. * - gives triggers upon changes in the data
  1146. * - can import/export data in various data formats
  1147. *
  1148. * @param {Array | DataTable} [data] Optional array with initial data
  1149. * @param {Object} [options] Available options:
  1150. * {String} fieldId Field name of the id in the
  1151. * items, 'id' by default.
  1152. * {Object.<String, String} convert
  1153. * A map with field names as key,
  1154. * and the field type as value.
  1155. * @constructor DataSet
  1156. */
  1157. // TODO: add a DataSet constructor DataSet(data, options)
  1158. function DataSet (data, options) {
  1159. this.id = util.randomUUID();
  1160. // correctly read optional arguments
  1161. if (data && !Array.isArray(data) && !util.isDataTable(data)) {
  1162. options = data;
  1163. data = null;
  1164. }
  1165. this.options = options || {};
  1166. this.data = {}; // map with data indexed by id
  1167. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1168. this.convert = {}; // field types by field name
  1169. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1170. if (this.options.convert) {
  1171. for (var field in this.options.convert) {
  1172. if (this.options.convert.hasOwnProperty(field)) {
  1173. var value = this.options.convert[field];
  1174. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1175. this.convert[field] = 'Date';
  1176. }
  1177. else {
  1178. this.convert[field] = value;
  1179. }
  1180. }
  1181. }
  1182. }
  1183. this.subscribers = {}; // event subscribers
  1184. this.internalIds = {}; // internally generated id's
  1185. // add initial data when provided
  1186. if (data) {
  1187. this.add(data);
  1188. }
  1189. }
  1190. /**
  1191. * Subscribe to an event, add an event listener
  1192. * @param {String} event Event name. Available events: 'put', 'update',
  1193. * 'remove'
  1194. * @param {function} callback Callback method. Called with three parameters:
  1195. * {String} event
  1196. * {Object | null} params
  1197. * {String | Number} senderId
  1198. */
  1199. DataSet.prototype.on = function on (event, callback) {
  1200. var subscribers = this.subscribers[event];
  1201. if (!subscribers) {
  1202. subscribers = [];
  1203. this.subscribers[event] = subscribers;
  1204. }
  1205. subscribers.push({
  1206. callback: callback
  1207. });
  1208. };
  1209. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1210. DataSet.prototype.subscribe = DataSet.prototype.on;
  1211. /**
  1212. * Unsubscribe from an event, remove an event listener
  1213. * @param {String} event
  1214. * @param {function} callback
  1215. */
  1216. DataSet.prototype.off = function off(event, callback) {
  1217. var subscribers = this.subscribers[event];
  1218. if (subscribers) {
  1219. this.subscribers[event] = subscribers.filter(function (listener) {
  1220. return (listener.callback != callback);
  1221. });
  1222. }
  1223. };
  1224. // TODO: make this function deprecated (replaced with `on` since version 0.5)
  1225. DataSet.prototype.unsubscribe = DataSet.prototype.off;
  1226. /**
  1227. * Trigger an event
  1228. * @param {String} event
  1229. * @param {Object | null} params
  1230. * @param {String} [senderId] Optional id of the sender.
  1231. * @private
  1232. */
  1233. DataSet.prototype._trigger = function (event, params, senderId) {
  1234. if (event == '*') {
  1235. throw new Error('Cannot trigger event *');
  1236. }
  1237. var subscribers = [];
  1238. if (event in this.subscribers) {
  1239. subscribers = subscribers.concat(this.subscribers[event]);
  1240. }
  1241. if ('*' in this.subscribers) {
  1242. subscribers = subscribers.concat(this.subscribers['*']);
  1243. }
  1244. for (var i = 0; i < subscribers.length; i++) {
  1245. var subscriber = subscribers[i];
  1246. if (subscriber.callback) {
  1247. subscriber.callback(event, params, senderId || null);
  1248. }
  1249. }
  1250. };
  1251. /**
  1252. * Add data.
  1253. * Adding an item will fail when there already is an item with the same id.
  1254. * @param {Object | Array | DataTable} data
  1255. * @param {String} [senderId] Optional sender id
  1256. * @return {Array} addedIds Array with the ids of the added items
  1257. */
  1258. DataSet.prototype.add = function (data, senderId) {
  1259. var addedIds = [],
  1260. id,
  1261. me = this;
  1262. if (data instanceof Array) {
  1263. // Array
  1264. for (var i = 0, len = data.length; i < len; i++) {
  1265. id = me._addItem(data[i]);
  1266. addedIds.push(id);
  1267. }
  1268. }
  1269. else if (util.isDataTable(data)) {
  1270. // Google DataTable
  1271. var columns = this._getColumnNames(data);
  1272. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1273. var item = {};
  1274. for (var col = 0, cols = columns.length; col < cols; col++) {
  1275. var field = columns[col];
  1276. item[field] = data.getValue(row, col);
  1277. }
  1278. id = me._addItem(item);
  1279. addedIds.push(id);
  1280. }
  1281. }
  1282. else if (data instanceof Object) {
  1283. // Single item
  1284. id = me._addItem(data);
  1285. addedIds.push(id);
  1286. }
  1287. else {
  1288. throw new Error('Unknown dataType');
  1289. }
  1290. if (addedIds.length) {
  1291. this._trigger('add', {items: addedIds}, senderId);
  1292. }
  1293. return addedIds;
  1294. };
  1295. /**
  1296. * Update existing items. When an item does not exist, it will be created
  1297. * @param {Object | Array | DataTable} data
  1298. * @param {String} [senderId] Optional sender id
  1299. * @return {Array} updatedIds The ids of the added or updated items
  1300. */
  1301. DataSet.prototype.update = function (data, senderId) {
  1302. var addedIds = [],
  1303. updatedIds = [],
  1304. me = this,
  1305. fieldId = me.fieldId;
  1306. var addOrUpdate = function (item) {
  1307. var id = item[fieldId];
  1308. if (me.data[id]) {
  1309. // update item
  1310. id = me._updateItem(item);
  1311. updatedIds.push(id);
  1312. }
  1313. else {
  1314. // add new item
  1315. id = me._addItem(item);
  1316. addedIds.push(id);
  1317. }
  1318. };
  1319. if (data instanceof Array) {
  1320. // Array
  1321. for (var i = 0, len = data.length; i < len; i++) {
  1322. addOrUpdate(data[i]);
  1323. }
  1324. }
  1325. else if (util.isDataTable(data)) {
  1326. // Google DataTable
  1327. var columns = this._getColumnNames(data);
  1328. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1329. var item = {};
  1330. for (var col = 0, cols = columns.length; col < cols; col++) {
  1331. var field = columns[col];
  1332. item[field] = data.getValue(row, col);
  1333. }
  1334. addOrUpdate(item);
  1335. }
  1336. }
  1337. else if (data instanceof Object) {
  1338. // Single item
  1339. addOrUpdate(data);
  1340. }
  1341. else {
  1342. throw new Error('Unknown dataType');
  1343. }
  1344. if (addedIds.length) {
  1345. this._trigger('add', {items: addedIds}, senderId);
  1346. }
  1347. if (updatedIds.length) {
  1348. this._trigger('update', {items: updatedIds}, senderId);
  1349. }
  1350. return addedIds.concat(updatedIds);
  1351. };
  1352. /**
  1353. * Get a data item or multiple items.
  1354. *
  1355. * Usage:
  1356. *
  1357. * get()
  1358. * get(options: Object)
  1359. * get(options: Object, data: Array | DataTable)
  1360. *
  1361. * get(id: Number | String)
  1362. * get(id: Number | String, options: Object)
  1363. * get(id: Number | String, options: Object, data: Array | DataTable)
  1364. *
  1365. * get(ids: Number[] | String[])
  1366. * get(ids: Number[] | String[], options: Object)
  1367. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1368. *
  1369. * Where:
  1370. *
  1371. * {Number | String} id The id of an item
  1372. * {Number[] | String{}} ids An array with ids of items
  1373. * {Object} options An Object with options. Available options:
  1374. * {String} [type] Type of data to be returned. Can
  1375. * be 'DataTable' or 'Array' (default)
  1376. * {Object.<String, String>} [convert]
  1377. * {String[]} [fields] field names to be returned
  1378. * {function} [filter] filter items
  1379. * {String | function} [order] Order the items by
  1380. * a field name or custom sort function.
  1381. * {Array | DataTable} [data] If provided, items will be appended to this
  1382. * array or table. Required in case of Google
  1383. * DataTable.
  1384. *
  1385. * @throws Error
  1386. */
  1387. DataSet.prototype.get = function (args) {
  1388. var me = this;
  1389. var globalShowInternalIds = this.showInternalIds;
  1390. // parse the arguments
  1391. var id, ids, options, data;
  1392. var firstType = util.getType(arguments[0]);
  1393. if (firstType == 'String' || firstType == 'Number') {
  1394. // get(id [, options] [, data])
  1395. id = arguments[0];
  1396. options = arguments[1];
  1397. data = arguments[2];
  1398. }
  1399. else if (firstType == 'Array') {
  1400. // get(ids [, options] [, data])
  1401. ids = arguments[0];
  1402. options = arguments[1];
  1403. data = arguments[2];
  1404. }
  1405. else {
  1406. // get([, options] [, data])
  1407. options = arguments[0];
  1408. data = arguments[1];
  1409. }
  1410. // determine the return type
  1411. var type;
  1412. if (options && options.type) {
  1413. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1414. if (data && (type != util.getType(data))) {
  1415. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1416. 'does not correspond with specified options.type (' + options.type + ')');
  1417. }
  1418. if (type == 'DataTable' && !util.isDataTable(data)) {
  1419. throw new Error('Parameter "data" must be a DataTable ' +
  1420. 'when options.type is "DataTable"');
  1421. }
  1422. }
  1423. else if (data) {
  1424. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1425. }
  1426. else {
  1427. type = 'Array';
  1428. }
  1429. // we allow the setting of this value for a single get request.
  1430. if (options != undefined) {
  1431. if (options.showInternalIds != undefined) {
  1432. this.showInternalIds = options.showInternalIds;
  1433. }
  1434. }
  1435. // build options
  1436. var convert = options && options.convert || this.options.convert;
  1437. var filter = options && options.filter;
  1438. var items = [], item, itemId, i, len;
  1439. // convert items
  1440. if (id != undefined) {
  1441. // return a single item
  1442. item = me._getItem(id, convert);
  1443. if (filter && !filter(item)) {
  1444. item = null;
  1445. }
  1446. }
  1447. else if (ids != undefined) {
  1448. // return a subset of items
  1449. for (i = 0, len = ids.length; i < len; i++) {
  1450. item = me._getItem(ids[i], convert);
  1451. if (!filter || filter(item)) {
  1452. items.push(item);
  1453. }
  1454. }
  1455. }
  1456. else {
  1457. // return all items
  1458. for (itemId in this.data) {
  1459. if (this.data.hasOwnProperty(itemId)) {
  1460. item = me._getItem(itemId, convert);
  1461. if (!filter || filter(item)) {
  1462. items.push(item);
  1463. }
  1464. }
  1465. }
  1466. }
  1467. // restore the global value of showInternalIds
  1468. this.showInternalIds = globalShowInternalIds;
  1469. // order the results
  1470. if (options && options.order && id == undefined) {
  1471. this._sort(items, options.order);
  1472. }
  1473. // filter fields of the items
  1474. if (options && options.fields) {
  1475. var fields = options.fields;
  1476. if (id != undefined) {
  1477. item = this._filterFields(item, fields);
  1478. }
  1479. else {
  1480. for (i = 0, len = items.length; i < len; i++) {
  1481. items[i] = this._filterFields(items[i], fields);
  1482. }
  1483. }
  1484. }
  1485. // return the results
  1486. if (type == 'DataTable') {
  1487. var columns = this._getColumnNames(data);
  1488. if (id != undefined) {
  1489. // append a single item to the data table
  1490. me._appendRow(data, columns, item);
  1491. }
  1492. else {
  1493. // copy the items to the provided data table
  1494. for (i = 0, len = items.length; i < len; i++) {
  1495. me._appendRow(data, columns, items[i]);
  1496. }
  1497. }
  1498. return data;
  1499. }
  1500. else {
  1501. // return an array
  1502. if (id != undefined) {
  1503. // a single item
  1504. return item;
  1505. }
  1506. else {
  1507. // multiple items
  1508. if (data) {
  1509. // copy the items to the provided array
  1510. for (i = 0, len = items.length; i < len; i++) {
  1511. data.push(items[i]);
  1512. }
  1513. return data;
  1514. }
  1515. else {
  1516. // just return our array
  1517. return items;
  1518. }
  1519. }
  1520. }
  1521. };
  1522. /**
  1523. * Get ids of all items or from a filtered set of items.
  1524. * @param {Object} [options] An Object with options. Available options:
  1525. * {function} [filter] filter items
  1526. * {String | function} [order] Order the items by
  1527. * a field name or custom sort function.
  1528. * @return {Array} ids
  1529. */
  1530. DataSet.prototype.getIds = function (options) {
  1531. var data = this.data,
  1532. filter = options && options.filter,
  1533. order = options && options.order,
  1534. convert = options && options.convert || this.options.convert,
  1535. i,
  1536. len,
  1537. id,
  1538. item,
  1539. items,
  1540. ids = [];
  1541. if (filter) {
  1542. // get filtered items
  1543. if (order) {
  1544. // create ordered list
  1545. items = [];
  1546. for (id in data) {
  1547. if (data.hasOwnProperty(id)) {
  1548. item = this._getItem(id, convert);
  1549. if (filter(item)) {
  1550. items.push(item);
  1551. }
  1552. }
  1553. }
  1554. this._sort(items, order);
  1555. for (i = 0, len = items.length; i < len; i++) {
  1556. ids[i] = items[i][this.fieldId];
  1557. }
  1558. }
  1559. else {
  1560. // create unordered list
  1561. for (id in data) {
  1562. if (data.hasOwnProperty(id)) {
  1563. item = this._getItem(id, convert);
  1564. if (filter(item)) {
  1565. ids.push(item[this.fieldId]);
  1566. }
  1567. }
  1568. }
  1569. }
  1570. }
  1571. else {
  1572. // get all items
  1573. if (order) {
  1574. // create an ordered list
  1575. items = [];
  1576. for (id in data) {
  1577. if (data.hasOwnProperty(id)) {
  1578. items.push(data[id]);
  1579. }
  1580. }
  1581. this._sort(items, order);
  1582. for (i = 0, len = items.length; i < len; i++) {
  1583. ids[i] = items[i][this.fieldId];
  1584. }
  1585. }
  1586. else {
  1587. // create unordered list
  1588. for (id in data) {
  1589. if (data.hasOwnProperty(id)) {
  1590. item = data[id];
  1591. ids.push(item[this.fieldId]);
  1592. }
  1593. }
  1594. }
  1595. }
  1596. return ids;
  1597. };
  1598. /**
  1599. * Execute a callback function for every item in the dataset.
  1600. * @param {function} callback
  1601. * @param {Object} [options] Available options:
  1602. * {Object.<String, String>} [convert]
  1603. * {String[]} [fields] filter fields
  1604. * {function} [filter] filter items
  1605. * {String | function} [order] Order the items by
  1606. * a field name or custom sort function.
  1607. */
  1608. DataSet.prototype.forEach = function (callback, options) {
  1609. var filter = options && options.filter,
  1610. convert = options && options.convert || this.options.convert,
  1611. data = this.data,
  1612. item,
  1613. id;
  1614. if (options && options.order) {
  1615. // execute forEach on ordered list
  1616. var items = this.get(options);
  1617. for (var i = 0, len = items.length; i < len; i++) {
  1618. item = items[i];
  1619. id = item[this.fieldId];
  1620. callback(item, id);
  1621. }
  1622. }
  1623. else {
  1624. // unordered
  1625. for (id in data) {
  1626. if (data.hasOwnProperty(id)) {
  1627. item = this._getItem(id, convert);
  1628. if (!filter || filter(item)) {
  1629. callback(item, id);
  1630. }
  1631. }
  1632. }
  1633. }
  1634. };
  1635. /**
  1636. * Map every item in the dataset.
  1637. * @param {function} callback
  1638. * @param {Object} [options] Available options:
  1639. * {Object.<String, String>} [convert]
  1640. * {String[]} [fields] filter fields
  1641. * {function} [filter] filter items
  1642. * {String | function} [order] Order the items by
  1643. * a field name or custom sort function.
  1644. * @return {Object[]} mappedItems
  1645. */
  1646. DataSet.prototype.map = function (callback, options) {
  1647. var filter = options && options.filter,
  1648. convert = options && options.convert || this.options.convert,
  1649. mappedItems = [],
  1650. data = this.data,
  1651. item;
  1652. // convert and filter items
  1653. for (var id in data) {
  1654. if (data.hasOwnProperty(id)) {
  1655. item = this._getItem(id, convert);
  1656. if (!filter || filter(item)) {
  1657. mappedItems.push(callback(item, id));
  1658. }
  1659. }
  1660. }
  1661. // order items
  1662. if (options && options.order) {
  1663. this._sort(mappedItems, options.order);
  1664. }
  1665. return mappedItems;
  1666. };
  1667. /**
  1668. * Filter the fields of an item
  1669. * @param {Object} item
  1670. * @param {String[]} fields Field names
  1671. * @return {Object} filteredItem
  1672. * @private
  1673. */
  1674. DataSet.prototype._filterFields = function (item, fields) {
  1675. var filteredItem = {};
  1676. for (var field in item) {
  1677. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1678. filteredItem[field] = item[field];
  1679. }
  1680. }
  1681. return filteredItem;
  1682. };
  1683. /**
  1684. * Sort the provided array with items
  1685. * @param {Object[]} items
  1686. * @param {String | function} order A field name or custom sort function.
  1687. * @private
  1688. */
  1689. DataSet.prototype._sort = function (items, order) {
  1690. if (util.isString(order)) {
  1691. // order by provided field name
  1692. var name = order; // field name
  1693. items.sort(function (a, b) {
  1694. var av = a[name];
  1695. var bv = b[name];
  1696. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1697. });
  1698. }
  1699. else if (typeof order === 'function') {
  1700. // order by sort function
  1701. items.sort(order);
  1702. }
  1703. // TODO: extend order by an Object {field:String, direction:String}
  1704. // where direction can be 'asc' or 'desc'
  1705. else {
  1706. throw new TypeError('Order must be a function or a string');
  1707. }
  1708. };
  1709. /**
  1710. * Remove an object by pointer or by id
  1711. * @param {String | Number | Object | Array} id Object or id, or an array with
  1712. * objects or ids to be removed
  1713. * @param {String} [senderId] Optional sender id
  1714. * @return {Array} removedIds
  1715. */
  1716. DataSet.prototype.remove = function (id, senderId) {
  1717. var removedIds = [],
  1718. i, len, removedId;
  1719. if (id instanceof Array) {
  1720. for (i = 0, len = id.length; i < len; i++) {
  1721. removedId = this._remove(id[i]);
  1722. if (removedId != null) {
  1723. removedIds.push(removedId);
  1724. }
  1725. }
  1726. }
  1727. else {
  1728. removedId = this._remove(id);
  1729. if (removedId != null) {
  1730. removedIds.push(removedId);
  1731. }
  1732. }
  1733. if (removedIds.length) {
  1734. this._trigger('remove', {items: removedIds}, senderId);
  1735. }
  1736. return removedIds;
  1737. };
  1738. /**
  1739. * Remove an item by its id
  1740. * @param {Number | String | Object} id id or item
  1741. * @returns {Number | String | null} id
  1742. * @private
  1743. */
  1744. DataSet.prototype._remove = function (id) {
  1745. if (util.isNumber(id) || util.isString(id)) {
  1746. if (this.data[id]) {
  1747. delete this.data[id];
  1748. delete this.internalIds[id];
  1749. return id;
  1750. }
  1751. }
  1752. else if (id instanceof Object) {
  1753. var itemId = id[this.fieldId];
  1754. if (itemId && this.data[itemId]) {
  1755. delete this.data[itemId];
  1756. delete this.internalIds[itemId];
  1757. return itemId;
  1758. }
  1759. }
  1760. return null;
  1761. };
  1762. /**
  1763. * Clear the data
  1764. * @param {String} [senderId] Optional sender id
  1765. * @return {Array} removedIds The ids of all removed items
  1766. */
  1767. DataSet.prototype.clear = function (senderId) {
  1768. var ids = Object.keys(this.data);
  1769. this.data = {};
  1770. this.internalIds = {};
  1771. this._trigger('remove', {items: ids}, senderId);
  1772. return ids;
  1773. };
  1774. /**
  1775. * Find the item with maximum value of a specified field
  1776. * @param {String} field
  1777. * @return {Object | null} item Item containing max value, or null if no items
  1778. */
  1779. DataSet.prototype.max = function (field) {
  1780. var data = this.data,
  1781. max = null,
  1782. maxField = null;
  1783. for (var id in data) {
  1784. if (data.hasOwnProperty(id)) {
  1785. var item = data[id];
  1786. var itemField = item[field];
  1787. if (itemField != null && (!max || itemField > maxField)) {
  1788. max = item;
  1789. maxField = itemField;
  1790. }
  1791. }
  1792. }
  1793. return max;
  1794. };
  1795. /**
  1796. * Find the item with minimum value of a specified field
  1797. * @param {String} field
  1798. * @return {Object | null} item Item containing max value, or null if no items
  1799. */
  1800. DataSet.prototype.min = function (field) {
  1801. var data = this.data,
  1802. min = null,
  1803. minField = null;
  1804. for (var id in data) {
  1805. if (data.hasOwnProperty(id)) {
  1806. var item = data[id];
  1807. var itemField = item[field];
  1808. if (itemField != null && (!min || itemField < minField)) {
  1809. min = item;
  1810. minField = itemField;
  1811. }
  1812. }
  1813. }
  1814. return min;
  1815. };
  1816. /**
  1817. * Find all distinct values of a specified field
  1818. * @param {String} field
  1819. * @return {Array} values Array containing all distinct values. If data items
  1820. * do not contain the specified field are ignored.
  1821. * The returned array is unordered.
  1822. */
  1823. DataSet.prototype.distinct = function (field) {
  1824. var data = this.data,
  1825. values = [],
  1826. fieldType = this.options.convert[field],
  1827. count = 0;
  1828. for (var prop in data) {
  1829. if (data.hasOwnProperty(prop)) {
  1830. var item = data[prop];
  1831. var value = util.convert(item[field], fieldType);
  1832. var exists = false;
  1833. for (var i = 0; i < count; i++) {
  1834. if (values[i] == value) {
  1835. exists = true;
  1836. break;
  1837. }
  1838. }
  1839. if (!exists && (value !== undefined)) {
  1840. values[count] = value;
  1841. count++;
  1842. }
  1843. }
  1844. }
  1845. return values;
  1846. };
  1847. /**
  1848. * Add a single item. Will fail when an item with the same id already exists.
  1849. * @param {Object} item
  1850. * @return {String} id
  1851. * @private
  1852. */
  1853. DataSet.prototype._addItem = function (item) {
  1854. var id = item[this.fieldId];
  1855. if (id != undefined) {
  1856. // check whether this id is already taken
  1857. if (this.data[id]) {
  1858. // item already exists
  1859. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1860. }
  1861. }
  1862. else {
  1863. // generate an id
  1864. id = util.randomUUID();
  1865. item[this.fieldId] = id;
  1866. this.internalIds[id] = item;
  1867. }
  1868. var d = {};
  1869. for (var field in item) {
  1870. if (item.hasOwnProperty(field)) {
  1871. var fieldType = this.convert[field]; // type may be undefined
  1872. d[field] = util.convert(item[field], fieldType);
  1873. }
  1874. }
  1875. this.data[id] = d;
  1876. return id;
  1877. };
  1878. /**
  1879. * Get an item. Fields can be converted to a specific type
  1880. * @param {String} id
  1881. * @param {Object.<String, String>} [convert] field types to convert
  1882. * @return {Object | null} item
  1883. * @private
  1884. */
  1885. DataSet.prototype._getItem = function (id, convert) {
  1886. var field, value;
  1887. // get the item from the dataset
  1888. var raw = this.data[id];
  1889. if (!raw) {
  1890. return null;
  1891. }
  1892. // convert the items field types
  1893. var converted = {},
  1894. fieldId = this.fieldId,
  1895. internalIds = this.internalIds;
  1896. if (convert) {
  1897. for (field in raw) {
  1898. if (raw.hasOwnProperty(field)) {
  1899. value = raw[field];
  1900. // output all fields, except internal ids
  1901. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1902. converted[field] = util.convert(value, convert[field]);
  1903. }
  1904. }
  1905. }
  1906. }
  1907. else {
  1908. // no field types specified, no converting needed
  1909. for (field in raw) {
  1910. if (raw.hasOwnProperty(field)) {
  1911. value = raw[field];
  1912. // output all fields, except internal ids
  1913. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1914. converted[field] = value;
  1915. }
  1916. }
  1917. }
  1918. }
  1919. return converted;
  1920. };
  1921. /**
  1922. * Update a single item: merge with existing item.
  1923. * Will fail when the item has no id, or when there does not exist an item
  1924. * with the same id.
  1925. * @param {Object} item
  1926. * @return {String} id
  1927. * @private
  1928. */
  1929. DataSet.prototype._updateItem = function (item) {
  1930. var id = item[this.fieldId];
  1931. if (id == undefined) {
  1932. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1933. }
  1934. var d = this.data[id];
  1935. if (!d) {
  1936. // item doesn't exist
  1937. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1938. }
  1939. // merge with current item
  1940. for (var field in item) {
  1941. if (item.hasOwnProperty(field)) {
  1942. var fieldType = this.convert[field]; // type may be undefined
  1943. d[field] = util.convert(item[field], fieldType);
  1944. }
  1945. }
  1946. return id;
  1947. };
  1948. /**
  1949. * check if an id is an internal or external id
  1950. * @param id
  1951. * @returns {boolean}
  1952. * @private
  1953. */
  1954. DataSet.prototype.isInternalId = function(id) {
  1955. return (id in this.internalIds);
  1956. };
  1957. /**
  1958. * Get an array with the column names of a Google DataTable
  1959. * @param {DataTable} dataTable
  1960. * @return {String[]} columnNames
  1961. * @private
  1962. */
  1963. DataSet.prototype._getColumnNames = function (dataTable) {
  1964. var columns = [];
  1965. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1966. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1967. }
  1968. return columns;
  1969. };
  1970. /**
  1971. * Append an item as a row to the dataTable
  1972. * @param dataTable
  1973. * @param columns
  1974. * @param item
  1975. * @private
  1976. */
  1977. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1978. var row = dataTable.addRow();
  1979. for (var col = 0, cols = columns.length; col < cols; col++) {
  1980. var field = columns[col];
  1981. dataTable.setValue(row, col, item[field]);
  1982. }
  1983. };
  1984. /**
  1985. * DataView
  1986. *
  1987. * a dataview offers a filtered view on a dataset or an other dataview.
  1988. *
  1989. * @param {DataSet | DataView} data
  1990. * @param {Object} [options] Available options: see method get
  1991. *
  1992. * @constructor DataView
  1993. */
  1994. function DataView (data, options) {
  1995. this.id = util.randomUUID();
  1996. this.data = null;
  1997. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1998. this.options = options || {};
  1999. this.fieldId = 'id'; // name of the field containing id
  2000. this.subscribers = {}; // event subscribers
  2001. var me = this;
  2002. this.listener = function () {
  2003. me._onEvent.apply(me, arguments);
  2004. };
  2005. this.setData(data);
  2006. }
  2007. // TODO: implement a function .config() to dynamically update things like configured filter
  2008. // and trigger changes accordingly
  2009. /**
  2010. * Set a data source for the view
  2011. * @param {DataSet | DataView} data
  2012. */
  2013. DataView.prototype.setData = function (data) {
  2014. var ids, dataItems, i, len;
  2015. if (this.data) {
  2016. // unsubscribe from current dataset
  2017. if (this.data.unsubscribe) {
  2018. this.data.unsubscribe('*', this.listener);
  2019. }
  2020. // trigger a remove of all items in memory
  2021. ids = [];
  2022. for (var id in this.ids) {
  2023. if (this.ids.hasOwnProperty(id)) {
  2024. ids.push(id);
  2025. }
  2026. }
  2027. this.ids = {};
  2028. this._trigger('remove', {items: ids});
  2029. }
  2030. this.data = data;
  2031. if (this.data) {
  2032. // update fieldId
  2033. this.fieldId = this.options.fieldId ||
  2034. (this.data && this.data.options && this.data.options.fieldId) ||
  2035. 'id';
  2036. // trigger an add of all added items
  2037. ids = this.data.getIds({filter: this.options && this.options.filter});
  2038. for (i = 0, len = ids.length; i < len; i++) {
  2039. id = ids[i];
  2040. this.ids[id] = true;
  2041. }
  2042. this._trigger('add', {items: ids});
  2043. // subscribe to new dataset
  2044. if (this.data.on) {
  2045. this.data.on('*', this.listener);
  2046. }
  2047. }
  2048. };
  2049. /**
  2050. * Get data from the data view
  2051. *
  2052. * Usage:
  2053. *
  2054. * get()
  2055. * get(options: Object)
  2056. * get(options: Object, data: Array | DataTable)
  2057. *
  2058. * get(id: Number)
  2059. * get(id: Number, options: Object)
  2060. * get(id: Number, options: Object, data: Array | DataTable)
  2061. *
  2062. * get(ids: Number[])
  2063. * get(ids: Number[], options: Object)
  2064. * get(ids: Number[], options: Object, data: Array | DataTable)
  2065. *
  2066. * Where:
  2067. *
  2068. * {Number | String} id The id of an item
  2069. * {Number[] | String{}} ids An array with ids of items
  2070. * {Object} options An Object with options. Available options:
  2071. * {String} [type] Type of data to be returned. Can
  2072. * be 'DataTable' or 'Array' (default)
  2073. * {Object.<String, String>} [convert]
  2074. * {String[]} [fields] field names to be returned
  2075. * {function} [filter] filter items
  2076. * {String | function} [order] Order the items by
  2077. * a field name or custom sort function.
  2078. * {Array | DataTable} [data] If provided, items will be appended to this
  2079. * array or table. Required in case of Google
  2080. * DataTable.
  2081. * @param args
  2082. */
  2083. DataView.prototype.get = function (args) {
  2084. var me = this;
  2085. // parse the arguments
  2086. var ids, options, data;
  2087. var firstType = util.getType(arguments[0]);
  2088. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2089. // get(id(s) [, options] [, data])
  2090. ids = arguments[0]; // can be a single id or an array with ids
  2091. options = arguments[1];
  2092. data = arguments[2];
  2093. }
  2094. else {
  2095. // get([, options] [, data])
  2096. options = arguments[0];
  2097. data = arguments[1];
  2098. }
  2099. // extend the options with the default options and provided options
  2100. var viewOptions = util.extend({}, this.options, options);
  2101. // create a combined filter method when needed
  2102. if (this.options.filter && options && options.filter) {
  2103. viewOptions.filter = function (item) {
  2104. return me.options.filter(item) && options.filter(item);
  2105. }
  2106. }
  2107. // build up the call to the linked data set
  2108. var getArguments = [];
  2109. if (ids != undefined) {
  2110. getArguments.push(ids);
  2111. }
  2112. getArguments.push(viewOptions);
  2113. getArguments.push(data);
  2114. return this.data && this.data.get.apply(this.data, getArguments);
  2115. };
  2116. /**
  2117. * Get ids of all items or from a filtered set of items.
  2118. * @param {Object} [options] An Object with options. Available options:
  2119. * {function} [filter] filter items
  2120. * {String | function} [order] Order the items by
  2121. * a field name or custom sort function.
  2122. * @return {Array} ids
  2123. */
  2124. DataView.prototype.getIds = function (options) {
  2125. var ids;
  2126. if (this.data) {
  2127. var defaultFilter = this.options.filter;
  2128. var filter;
  2129. if (options && options.filter) {
  2130. if (defaultFilter) {
  2131. filter = function (item) {
  2132. return defaultFilter(item) && options.filter(item);
  2133. }
  2134. }
  2135. else {
  2136. filter = options.filter;
  2137. }
  2138. }
  2139. else {
  2140. filter = defaultFilter;
  2141. }
  2142. ids = this.data.getIds({
  2143. filter: filter,
  2144. order: options && options.order
  2145. });
  2146. }
  2147. else {
  2148. ids = [];
  2149. }
  2150. return ids;
  2151. };
  2152. /**
  2153. * Event listener. Will propagate all events from the connected data set to
  2154. * the subscribers of the DataView, but will filter the items and only trigger
  2155. * when there are changes in the filtered data set.
  2156. * @param {String} event
  2157. * @param {Object | null} params
  2158. * @param {String} senderId
  2159. * @private
  2160. */
  2161. DataView.prototype._onEvent = function (event, params, senderId) {
  2162. var i, len, id, item,
  2163. ids = params && params.items,
  2164. data = this.data,
  2165. added = [],
  2166. updated = [],
  2167. removed = [];
  2168. if (ids && data) {
  2169. switch (event) {
  2170. case 'add':
  2171. // filter the ids of the added items
  2172. for (i = 0, len = ids.length; i < len; i++) {
  2173. id = ids[i];
  2174. item = this.get(id);
  2175. if (item) {
  2176. this.ids[id] = true;
  2177. added.push(id);
  2178. }
  2179. }
  2180. break;
  2181. case 'update':
  2182. // determine the event from the views viewpoint: an updated
  2183. // item can be added, updated, or removed from this view.
  2184. for (i = 0, len = ids.length; i < len; i++) {
  2185. id = ids[i];
  2186. item = this.get(id);
  2187. if (item) {
  2188. if (this.ids[id]) {
  2189. updated.push(id);
  2190. }
  2191. else {
  2192. this.ids[id] = true;
  2193. added.push(id);
  2194. }
  2195. }
  2196. else {
  2197. if (this.ids[id]) {
  2198. delete this.ids[id];
  2199. removed.push(id);
  2200. }
  2201. else {
  2202. // nothing interesting for me :-(
  2203. }
  2204. }
  2205. }
  2206. break;
  2207. case 'remove':
  2208. // filter the ids of the removed items
  2209. for (i = 0, len = ids.length; i < len; i++) {
  2210. id = ids[i];
  2211. if (this.ids[id]) {
  2212. delete this.ids[id];
  2213. removed.push(id);
  2214. }
  2215. }
  2216. break;
  2217. }
  2218. if (added.length) {
  2219. this._trigger('add', {items: added}, senderId);
  2220. }
  2221. if (updated.length) {
  2222. this._trigger('update', {items: updated}, senderId);
  2223. }
  2224. if (removed.length) {
  2225. this._trigger('remove', {items: removed}, senderId);
  2226. }
  2227. }
  2228. };
  2229. // copy subscription functionality from DataSet
  2230. DataView.prototype.on = DataSet.prototype.on;
  2231. DataView.prototype.off = DataSet.prototype.off;
  2232. DataView.prototype._trigger = DataSet.prototype._trigger;
  2233. // TODO: make these functions deprecated (replaced with `on` and `off` since version 0.5)
  2234. DataView.prototype.subscribe = DataView.prototype.on;
  2235. DataView.prototype.unsubscribe = DataView.prototype.off;
  2236. /**
  2237. * Utility functions for ordering and stacking of items
  2238. */
  2239. var stack = {};
  2240. /**
  2241. * Order items by their start data
  2242. * @param {Item[]} items
  2243. */
  2244. stack.orderByStart = function orderByStart(items) {
  2245. items.sort(function (a, b) {
  2246. return a.data.start - b.data.start;
  2247. });
  2248. };
  2249. /**
  2250. * Order items by their end date. If they have no end date, their start date
  2251. * is used.
  2252. * @param {Item[]} items
  2253. */
  2254. stack.orderByEnd = function orderByEnd(items) {
  2255. items.sort(function (a, b) {
  2256. var aTime = ('end' in a.data) ? a.data.end : a.data.start,
  2257. bTime = ('end' in b.data) ? b.data.end : b.data.start;
  2258. return aTime - bTime;
  2259. });
  2260. };
  2261. /**
  2262. * Adjust vertical positions of the items such that they don't overlap each
  2263. * other.
  2264. * @param {Item[]} items
  2265. * All visible items
  2266. * @param {{item: number, axis: number}} margin
  2267. * Margins between items and between items and the axis.
  2268. * @param {boolean} [force=false]
  2269. * If true, all items will be repositioned. If false (default), only
  2270. * items having a top===null will be re-stacked
  2271. */
  2272. stack.stack = function _stack (items, margin, force) {
  2273. var i, iMax;
  2274. if (force) {
  2275. // reset top position of all items
  2276. for (i = 0, iMax = items.length; i < iMax; i++) {
  2277. items[i].top = null;
  2278. }
  2279. }
  2280. // calculate new, non-overlapping positions
  2281. for (i = 0, iMax = items.length; i < iMax; i++) {
  2282. var item = items[i];
  2283. if (item.top === null) {
  2284. // initialize top position
  2285. item.top = margin.axis;
  2286. do {
  2287. // TODO: optimize checking for overlap. when there is a gap without items,
  2288. // you only need to check for items from the next item on, not from zero
  2289. var collidingItem = null;
  2290. for (var j = 0, jj = items.length; j < jj; j++) {
  2291. var other = items[j];
  2292. if (other.top !== null && other !== item && stack.collision(item, other, margin.item)) {
  2293. collidingItem = other;
  2294. break;
  2295. }
  2296. }
  2297. if (collidingItem != null) {
  2298. // There is a collision. Reposition the items above the colliding element
  2299. item.top = collidingItem.top + collidingItem.height + margin.item;
  2300. }
  2301. } while (collidingItem);
  2302. }
  2303. }
  2304. };
  2305. /**
  2306. * Adjust vertical positions of the items without stacking them
  2307. * @param {Item[]} items
  2308. * All visible items
  2309. * @param {{item: number, axis: number}} margin
  2310. * Margins between items and between items and the axis.
  2311. */
  2312. stack.nostack = function nostack (items, margin) {
  2313. var i, iMax;
  2314. // reset top position of all items
  2315. for (i = 0, iMax = items.length; i < iMax; i++) {
  2316. items[i].top = margin.axis;
  2317. }
  2318. };
  2319. /**
  2320. * Test if the two provided items collide
  2321. * The items must have parameters left, width, top, and height.
  2322. * @param {Item} a The first item
  2323. * @param {Item} b The second item
  2324. * @param {Number} margin A minimum required margin.
  2325. * If margin is provided, the two items will be
  2326. * marked colliding when they overlap or
  2327. * when the margin between the two is smaller than
  2328. * the requested margin.
  2329. * @return {boolean} true if a and b collide, else false
  2330. */
  2331. stack.collision = function collision (a, b, margin) {
  2332. return ((a.left - margin) < (b.left + b.width) &&
  2333. (a.left + a.width + margin) > b.left &&
  2334. (a.top - margin) < (b.top + b.height) &&
  2335. (a.top + a.height + margin) > b.top);
  2336. };
  2337. /**
  2338. * @constructor TimeStep
  2339. * The class TimeStep is an iterator for dates. You provide a start date and an
  2340. * end date. The class itself determines the best scale (step size) based on the
  2341. * provided start Date, end Date, and minimumStep.
  2342. *
  2343. * If minimumStep is provided, the step size is chosen as close as possible
  2344. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2345. * provided, the scale is set to 1 DAY.
  2346. * The minimumStep should correspond with the onscreen size of about 6 characters
  2347. *
  2348. * Alternatively, you can set a scale by hand.
  2349. * After creation, you can initialize the class by executing first(). Then you
  2350. * can iterate from the start date to the end date via next(). You can check if
  2351. * the end date is reached with the function hasNext(). After each step, you can
  2352. * retrieve the current date via getCurrent().
  2353. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2354. * days, to years.
  2355. *
  2356. * Version: 1.2
  2357. *
  2358. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2359. * or new Date(2010, 9, 21, 23, 45, 00)
  2360. * @param {Date} [end] The end date
  2361. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2362. */
  2363. function TimeStep(start, end, minimumStep) {
  2364. // variables
  2365. this.current = new Date();
  2366. this._start = new Date();
  2367. this._end = new Date();
  2368. this.autoScale = true;
  2369. this.scale = TimeStep.SCALE.DAY;
  2370. this.step = 1;
  2371. // initialize the range
  2372. this.setRange(start, end, minimumStep);
  2373. }
  2374. /// enum scale
  2375. TimeStep.SCALE = {
  2376. MILLISECOND: 1,
  2377. SECOND: 2,
  2378. MINUTE: 3,
  2379. HOUR: 4,
  2380. DAY: 5,
  2381. WEEKDAY: 6,
  2382. MONTH: 7,
  2383. YEAR: 8
  2384. };
  2385. /**
  2386. * Set a new range
  2387. * If minimumStep is provided, the step size is chosen as close as possible
  2388. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2389. * provided, the scale is set to 1 DAY.
  2390. * The minimumStep should correspond with the onscreen size of about 6 characters
  2391. * @param {Date} [start] The start date and time.
  2392. * @param {Date} [end] The end date and time.
  2393. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2394. */
  2395. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2396. if (!(start instanceof Date) || !(end instanceof Date)) {
  2397. throw "No legal start or end date in method setRange";
  2398. }
  2399. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2400. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2401. if (this.autoScale) {
  2402. this.setMinimumStep(minimumStep);
  2403. }
  2404. };
  2405. /**
  2406. * Set the range iterator to the start date.
  2407. */
  2408. TimeStep.prototype.first = function() {
  2409. this.current = new Date(this._start.valueOf());
  2410. this.roundToMinor();
  2411. };
  2412. /**
  2413. * Round the current date to the first minor date value
  2414. * This must be executed once when the current date is set to start Date
  2415. */
  2416. TimeStep.prototype.roundToMinor = function() {
  2417. // round to floor
  2418. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2419. //noinspection FallthroughInSwitchStatementJS
  2420. switch (this.scale) {
  2421. case TimeStep.SCALE.YEAR:
  2422. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2423. this.current.setMonth(0);
  2424. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2425. case TimeStep.SCALE.DAY: // intentional fall through
  2426. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2427. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2428. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2429. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2430. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2431. }
  2432. if (this.step != 1) {
  2433. // round down to the first minor value that is a multiple of the current step size
  2434. switch (this.scale) {
  2435. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2436. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2437. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2438. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2439. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2440. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2441. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2442. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2443. default: break;
  2444. }
  2445. }
  2446. };
  2447. /**
  2448. * Check if the there is a next step
  2449. * @return {boolean} true if the current date has not passed the end date
  2450. */
  2451. TimeStep.prototype.hasNext = function () {
  2452. return (this.current.valueOf() <= this._end.valueOf());
  2453. };
  2454. /**
  2455. * Do the next step
  2456. */
  2457. TimeStep.prototype.next = function() {
  2458. var prev = this.current.valueOf();
  2459. // Two cases, needed to prevent issues with switching daylight savings
  2460. // (end of March and end of October)
  2461. if (this.current.getMonth() < 6) {
  2462. switch (this.scale) {
  2463. case TimeStep.SCALE.MILLISECOND:
  2464. this.current = new Date(this.current.valueOf() + this.step); break;
  2465. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2466. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2467. case TimeStep.SCALE.HOUR:
  2468. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2469. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2470. var h = this.current.getHours();
  2471. this.current.setHours(h - (h % this.step));
  2472. break;
  2473. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2474. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2475. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2476. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2477. default: break;
  2478. }
  2479. }
  2480. else {
  2481. switch (this.scale) {
  2482. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2483. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2484. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2485. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2486. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2487. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2488. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2489. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2490. default: break;
  2491. }
  2492. }
  2493. if (this.step != 1) {
  2494. // round down to the correct major value
  2495. switch (this.scale) {
  2496. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2497. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2498. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2499. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2500. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2501. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2502. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2503. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2504. default: break;
  2505. }
  2506. }
  2507. // safety mechanism: if current time is still unchanged, move to the end
  2508. if (this.current.valueOf() == prev) {
  2509. this.current = new Date(this._end.valueOf());
  2510. }
  2511. };
  2512. /**
  2513. * Get the current datetime
  2514. * @return {Date} current The current date
  2515. */
  2516. TimeStep.prototype.getCurrent = function() {
  2517. return this.current;
  2518. };
  2519. /**
  2520. * Set a custom scale. Autoscaling will be disabled.
  2521. * For example setScale(SCALE.MINUTES, 5) will result
  2522. * in minor steps of 5 minutes, and major steps of an hour.
  2523. *
  2524. * @param {TimeStep.SCALE} newScale
  2525. * A scale. Choose from SCALE.MILLISECOND,
  2526. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2527. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2528. * SCALE.YEAR.
  2529. * @param {Number} newStep A step size, by default 1. Choose for
  2530. * example 1, 2, 5, or 10.
  2531. */
  2532. TimeStep.prototype.setScale = function(newScale, newStep) {
  2533. this.scale = newScale;
  2534. if (newStep > 0) {
  2535. this.step = newStep;
  2536. }
  2537. this.autoScale = false;
  2538. };
  2539. /**
  2540. * Enable or disable autoscaling
  2541. * @param {boolean} enable If true, autoascaling is set true
  2542. */
  2543. TimeStep.prototype.setAutoScale = function (enable) {
  2544. this.autoScale = enable;
  2545. };
  2546. /**
  2547. * Automatically determine the scale that bests fits the provided minimum step
  2548. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2549. */
  2550. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2551. if (minimumStep == undefined) {
  2552. return;
  2553. }
  2554. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2555. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2556. var stepDay = (1000 * 60 * 60 * 24);
  2557. var stepHour = (1000 * 60 * 60);
  2558. var stepMinute = (1000 * 60);
  2559. var stepSecond = (1000);
  2560. var stepMillisecond= (1);
  2561. // find the smallest step that is larger than the provided minimumStep
  2562. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2563. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2564. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2565. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2566. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2567. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2568. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2569. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2570. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2571. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2572. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2573. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2574. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2575. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2576. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2577. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2578. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2579. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2580. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2581. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2582. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2583. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2584. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2585. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2586. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2587. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2588. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2589. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2590. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2591. };
  2592. /**
  2593. * Snap a date to a rounded value.
  2594. * The snap intervals are dependent on the current scale and step.
  2595. * @param {Date} date the date to be snapped.
  2596. * @return {Date} snappedDate
  2597. */
  2598. TimeStep.prototype.snap = function(date) {
  2599. var clone = new Date(date.valueOf());
  2600. if (this.scale == TimeStep.SCALE.YEAR) {
  2601. var year = clone.getFullYear() + Math.round(clone.getMonth() / 12);
  2602. clone.setFullYear(Math.round(year / this.step) * this.step);
  2603. clone.setMonth(0);
  2604. clone.setDate(0);
  2605. clone.setHours(0);
  2606. clone.setMinutes(0);
  2607. clone.setSeconds(0);
  2608. clone.setMilliseconds(0);
  2609. }
  2610. else if (this.scale == TimeStep.SCALE.MONTH) {
  2611. if (clone.getDate() > 15) {
  2612. clone.setDate(1);
  2613. clone.setMonth(clone.getMonth() + 1);
  2614. // important: first set Date to 1, after that change the month.
  2615. }
  2616. else {
  2617. clone.setDate(1);
  2618. }
  2619. clone.setHours(0);
  2620. clone.setMinutes(0);
  2621. clone.setSeconds(0);
  2622. clone.setMilliseconds(0);
  2623. }
  2624. else if (this.scale == TimeStep.SCALE.DAY) {
  2625. //noinspection FallthroughInSwitchStatementJS
  2626. switch (this.step) {
  2627. case 5:
  2628. case 2:
  2629. clone.setHours(Math.round(clone.getHours() / 24) * 24); break;
  2630. default:
  2631. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2632. }
  2633. clone.setMinutes(0);
  2634. clone.setSeconds(0);
  2635. clone.setMilliseconds(0);
  2636. }
  2637. else if (this.scale == TimeStep.SCALE.WEEKDAY) {
  2638. //noinspection FallthroughInSwitchStatementJS
  2639. switch (this.step) {
  2640. case 5:
  2641. case 2:
  2642. clone.setHours(Math.round(clone.getHours() / 12) * 12); break;
  2643. default:
  2644. clone.setHours(Math.round(clone.getHours() / 6) * 6); break;
  2645. }
  2646. clone.setMinutes(0);
  2647. clone.setSeconds(0);
  2648. clone.setMilliseconds(0);
  2649. }
  2650. else if (this.scale == TimeStep.SCALE.HOUR) {
  2651. switch (this.step) {
  2652. case 4:
  2653. clone.setMinutes(Math.round(clone.getMinutes() / 60) * 60); break;
  2654. default:
  2655. clone.setMinutes(Math.round(clone.getMinutes() / 30) * 30); break;
  2656. }
  2657. clone.setSeconds(0);
  2658. clone.setMilliseconds(0);
  2659. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2660. //noinspection FallthroughInSwitchStatementJS
  2661. switch (this.step) {
  2662. case 15:
  2663. case 10:
  2664. clone.setMinutes(Math.round(clone.getMinutes() / 5) * 5);
  2665. clone.setSeconds(0);
  2666. break;
  2667. case 5:
  2668. clone.setSeconds(Math.round(clone.getSeconds() / 60) * 60); break;
  2669. default:
  2670. clone.setSeconds(Math.round(clone.getSeconds() / 30) * 30); break;
  2671. }
  2672. clone.setMilliseconds(0);
  2673. }
  2674. else if (this.scale == TimeStep.SCALE.SECOND) {
  2675. //noinspection FallthroughInSwitchStatementJS
  2676. switch (this.step) {
  2677. case 15:
  2678. case 10:
  2679. clone.setSeconds(Math.round(clone.getSeconds() / 5) * 5);
  2680. clone.setMilliseconds(0);
  2681. break;
  2682. case 5:
  2683. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 1000) * 1000); break;
  2684. default:
  2685. clone.setMilliseconds(Math.round(clone.getMilliseconds() / 500) * 500); break;
  2686. }
  2687. }
  2688. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2689. var step = this.step > 5 ? this.step / 2 : 1;
  2690. clone.setMilliseconds(Math.round(clone.getMilliseconds() / step) * step);
  2691. }
  2692. return clone;
  2693. };
  2694. /**
  2695. * Check if the current value is a major value (for example when the step
  2696. * is DAY, a major value is each first day of the MONTH)
  2697. * @return {boolean} true if current date is major, else false.
  2698. */
  2699. TimeStep.prototype.isMajor = function() {
  2700. switch (this.scale) {
  2701. case TimeStep.SCALE.MILLISECOND:
  2702. return (this.current.getMilliseconds() == 0);
  2703. case TimeStep.SCALE.SECOND:
  2704. return (this.current.getSeconds() == 0);
  2705. case TimeStep.SCALE.MINUTE:
  2706. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2707. // Note: this is no bug. Major label is equal for both minute and hour scale
  2708. case TimeStep.SCALE.HOUR:
  2709. return (this.current.getHours() == 0);
  2710. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2711. case TimeStep.SCALE.DAY:
  2712. return (this.current.getDate() == 1);
  2713. case TimeStep.SCALE.MONTH:
  2714. return (this.current.getMonth() == 0);
  2715. case TimeStep.SCALE.YEAR:
  2716. return false;
  2717. default:
  2718. return false;
  2719. }
  2720. };
  2721. /**
  2722. * Returns formatted text for the minor axislabel, depending on the current
  2723. * date and the scale. For example when scale is MINUTE, the current time is
  2724. * formatted as "hh:mm".
  2725. * @param {Date} [date] custom date. if not provided, current date is taken
  2726. */
  2727. TimeStep.prototype.getLabelMinor = function(date) {
  2728. if (date == undefined) {
  2729. date = this.current;
  2730. }
  2731. switch (this.scale) {
  2732. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2733. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2734. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2735. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2736. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2737. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2738. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2739. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2740. default: return '';
  2741. }
  2742. };
  2743. /**
  2744. * Returns formatted text for the major axis label, depending on the current
  2745. * date and the scale. For example when scale is MINUTE, the major scale is
  2746. * hours, and the hour will be formatted as "hh".
  2747. * @param {Date} [date] custom date. if not provided, current date is taken
  2748. */
  2749. TimeStep.prototype.getLabelMajor = function(date) {
  2750. if (date == undefined) {
  2751. date = this.current;
  2752. }
  2753. //noinspection FallthroughInSwitchStatementJS
  2754. switch (this.scale) {
  2755. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2756. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2757. case TimeStep.SCALE.MINUTE:
  2758. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2759. case TimeStep.SCALE.WEEKDAY:
  2760. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2761. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2762. case TimeStep.SCALE.YEAR: return '';
  2763. default: return '';
  2764. }
  2765. };
  2766. /**
  2767. * @constructor Range
  2768. * A Range controls a numeric range with a start and end value.
  2769. * The Range adjusts the range based on mouse events or programmatic changes,
  2770. * and triggers events when the range is changing or has been changed.
  2771. * @param {RootPanel} root Root panel, used to subscribe to events
  2772. * @param {Panel} parent Parent panel, used to attach to the DOM
  2773. * @param {Object} [options] See description at Range.setOptions
  2774. */
  2775. function Range(root, parent, options) {
  2776. this.id = util.randomUUID();
  2777. this.start = null; // Number
  2778. this.end = null; // Number
  2779. this.root = root;
  2780. this.parent = parent;
  2781. this.options = options || {};
  2782. // drag listeners for dragging
  2783. this.root.on('dragstart', this._onDragStart.bind(this));
  2784. this.root.on('drag', this._onDrag.bind(this));
  2785. this.root.on('dragend', this._onDragEnd.bind(this));
  2786. // ignore dragging when holding
  2787. this.root.on('hold', this._onHold.bind(this));
  2788. // mouse wheel for zooming
  2789. this.root.on('mousewheel', this._onMouseWheel.bind(this));
  2790. this.root.on('DOMMouseScroll', this._onMouseWheel.bind(this)); // For FF
  2791. // pinch to zoom
  2792. this.root.on('touch', this._onTouch.bind(this));
  2793. this.root.on('pinch', this._onPinch.bind(this));
  2794. this.setOptions(options);
  2795. }
  2796. // turn Range into an event emitter
  2797. Emitter(Range.prototype);
  2798. /**
  2799. * Set options for the range controller
  2800. * @param {Object} options Available options:
  2801. * {Number} min Minimum value for start
  2802. * {Number} max Maximum value for end
  2803. * {Number} zoomMin Set a minimum value for
  2804. * (end - start).
  2805. * {Number} zoomMax Set a maximum value for
  2806. * (end - start).
  2807. */
  2808. Range.prototype.setOptions = function (options) {
  2809. util.extend(this.options, options);
  2810. // re-apply range with new limitations
  2811. if (this.start !== null && this.end !== null) {
  2812. this.setRange(this.start, this.end);
  2813. }
  2814. };
  2815. /**
  2816. * Test whether direction has a valid value
  2817. * @param {String} direction 'horizontal' or 'vertical'
  2818. */
  2819. function validateDirection (direction) {
  2820. if (direction != 'horizontal' && direction != 'vertical') {
  2821. throw new TypeError('Unknown direction "' + direction + '". ' +
  2822. 'Choose "horizontal" or "vertical".');
  2823. }
  2824. }
  2825. /**
  2826. * Set a new start and end range
  2827. * @param {Number} [start]
  2828. * @param {Number} [end]
  2829. */
  2830. Range.prototype.setRange = function(start, end) {
  2831. var changed = this._applyRange(start, end);
  2832. if (changed) {
  2833. var params = {
  2834. start: new Date(this.start),
  2835. end: new Date(this.end)
  2836. };
  2837. this.emit('rangechange', params);
  2838. this.emit('rangechanged', params);
  2839. }
  2840. };
  2841. /**
  2842. * Set a new start and end range. This method is the same as setRange, but
  2843. * does not trigger a range change and range changed event, and it returns
  2844. * true when the range is changed
  2845. * @param {Number} [start]
  2846. * @param {Number} [end]
  2847. * @return {Boolean} changed
  2848. * @private
  2849. */
  2850. Range.prototype._applyRange = function(start, end) {
  2851. var newStart = (start != null) ? util.convert(start, 'Date').valueOf() : this.start,
  2852. newEnd = (end != null) ? util.convert(end, 'Date').valueOf() : this.end,
  2853. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2854. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2855. diff;
  2856. // check for valid number
  2857. if (isNaN(newStart) || newStart === null) {
  2858. throw new Error('Invalid start "' + start + '"');
  2859. }
  2860. if (isNaN(newEnd) || newEnd === null) {
  2861. throw new Error('Invalid end "' + end + '"');
  2862. }
  2863. // prevent start < end
  2864. if (newEnd < newStart) {
  2865. newEnd = newStart;
  2866. }
  2867. // prevent start < min
  2868. if (min !== null) {
  2869. if (newStart < min) {
  2870. diff = (min - newStart);
  2871. newStart += diff;
  2872. newEnd += diff;
  2873. // prevent end > max
  2874. if (max != null) {
  2875. if (newEnd > max) {
  2876. newEnd = max;
  2877. }
  2878. }
  2879. }
  2880. }
  2881. // prevent end > max
  2882. if (max !== null) {
  2883. if (newEnd > max) {
  2884. diff = (newEnd - max);
  2885. newStart -= diff;
  2886. newEnd -= diff;
  2887. // prevent start < min
  2888. if (min != null) {
  2889. if (newStart < min) {
  2890. newStart = min;
  2891. }
  2892. }
  2893. }
  2894. }
  2895. // prevent (end-start) < zoomMin
  2896. if (this.options.zoomMin !== null) {
  2897. var zoomMin = parseFloat(this.options.zoomMin);
  2898. if (zoomMin < 0) {
  2899. zoomMin = 0;
  2900. }
  2901. if ((newEnd - newStart) < zoomMin) {
  2902. if ((this.end - this.start) === zoomMin) {
  2903. // ignore this action, we are already zoomed to the minimum
  2904. newStart = this.start;
  2905. newEnd = this.end;
  2906. }
  2907. else {
  2908. // zoom to the minimum
  2909. diff = (zoomMin - (newEnd - newStart));
  2910. newStart -= diff / 2;
  2911. newEnd += diff / 2;
  2912. }
  2913. }
  2914. }
  2915. // prevent (end-start) > zoomMax
  2916. if (this.options.zoomMax !== null) {
  2917. var zoomMax = parseFloat(this.options.zoomMax);
  2918. if (zoomMax < 0) {
  2919. zoomMax = 0;
  2920. }
  2921. if ((newEnd - newStart) > zoomMax) {
  2922. if ((this.end - this.start) === zoomMax) {
  2923. // ignore this action, we are already zoomed to the maximum
  2924. newStart = this.start;
  2925. newEnd = this.end;
  2926. }
  2927. else {
  2928. // zoom to the maximum
  2929. diff = ((newEnd - newStart) - zoomMax);
  2930. newStart += diff / 2;
  2931. newEnd -= diff / 2;
  2932. }
  2933. }
  2934. }
  2935. var changed = (this.start != newStart || this.end != newEnd);
  2936. this.start = newStart;
  2937. this.end = newEnd;
  2938. return changed;
  2939. };
  2940. /**
  2941. * Retrieve the current range.
  2942. * @return {Object} An object with start and end properties
  2943. */
  2944. Range.prototype.getRange = function() {
  2945. return {
  2946. start: this.start,
  2947. end: this.end
  2948. };
  2949. };
  2950. /**
  2951. * Calculate the conversion offset and scale for current range, based on
  2952. * the provided width
  2953. * @param {Number} width
  2954. * @returns {{offset: number, scale: number}} conversion
  2955. */
  2956. Range.prototype.conversion = function (width) {
  2957. return Range.conversion(this.start, this.end, width);
  2958. };
  2959. /**
  2960. * Static method to calculate the conversion offset and scale for a range,
  2961. * based on the provided start, end, and width
  2962. * @param {Number} start
  2963. * @param {Number} end
  2964. * @param {Number} width
  2965. * @returns {{offset: number, scale: number}} conversion
  2966. */
  2967. Range.conversion = function (start, end, width) {
  2968. if (width != 0 && (end - start != 0)) {
  2969. return {
  2970. offset: start,
  2971. scale: width / (end - start)
  2972. }
  2973. }
  2974. else {
  2975. return {
  2976. offset: 0,
  2977. scale: 1
  2978. };
  2979. }
  2980. };
  2981. // global (private) object to store drag params
  2982. var touchParams = {};
  2983. /**
  2984. * Start dragging horizontally or vertically
  2985. * @param {Event} event
  2986. * @private
  2987. */
  2988. Range.prototype._onDragStart = function(event) {
  2989. // refuse to drag when we where pinching to prevent the timeline make a jump
  2990. // when releasing the fingers in opposite order from the touch screen
  2991. if (touchParams.ignore) return;
  2992. // TODO: reckon with option movable
  2993. touchParams.start = this.start;
  2994. touchParams.end = this.end;
  2995. var frame = this.parent.frame;
  2996. if (frame) {
  2997. frame.style.cursor = 'move';
  2998. }
  2999. };
  3000. /**
  3001. * Perform dragging operating.
  3002. * @param {Event} event
  3003. * @private
  3004. */
  3005. Range.prototype._onDrag = function (event) {
  3006. var direction = this.options.direction;
  3007. validateDirection(direction);
  3008. // TODO: reckon with option movable
  3009. // refuse to drag when we where pinching to prevent the timeline make a jump
  3010. // when releasing the fingers in opposite order from the touch screen
  3011. if (touchParams.ignore) return;
  3012. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3013. interval = (touchParams.end - touchParams.start),
  3014. width = (direction == 'horizontal') ? this.parent.width : this.parent.height,
  3015. diffRange = -delta / width * interval;
  3016. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3017. this.emit('rangechange', {
  3018. start: new Date(this.start),
  3019. end: new Date(this.end)
  3020. });
  3021. };
  3022. /**
  3023. * Stop dragging operating.
  3024. * @param {event} event
  3025. * @private
  3026. */
  3027. Range.prototype._onDragEnd = function (event) {
  3028. // refuse to drag when we where pinching to prevent the timeline make a jump
  3029. // when releasing the fingers in opposite order from the touch screen
  3030. if (touchParams.ignore) return;
  3031. // TODO: reckon with option movable
  3032. if (this.parent.frame) {
  3033. this.parent.frame.style.cursor = 'auto';
  3034. }
  3035. // fire a rangechanged event
  3036. this.emit('rangechanged', {
  3037. start: new Date(this.start),
  3038. end: new Date(this.end)
  3039. });
  3040. };
  3041. /**
  3042. * Event handler for mouse wheel event, used to zoom
  3043. * Code from http://adomas.org/javascript-mouse-wheel/
  3044. * @param {Event} event
  3045. * @private
  3046. */
  3047. Range.prototype._onMouseWheel = function(event) {
  3048. // TODO: reckon with option zoomable
  3049. // retrieve delta
  3050. var delta = 0;
  3051. if (event.wheelDelta) { /* IE/Opera. */
  3052. delta = event.wheelDelta / 120;
  3053. } else if (event.detail) { /* Mozilla case. */
  3054. // In Mozilla, sign of delta is different than in IE.
  3055. // Also, delta is multiple of 3.
  3056. delta = -event.detail / 3;
  3057. }
  3058. // If delta is nonzero, handle it.
  3059. // Basically, delta is now positive if wheel was scrolled up,
  3060. // and negative, if wheel was scrolled down.
  3061. if (delta) {
  3062. // perform the zoom action. Delta is normally 1 or -1
  3063. // adjust a negative delta such that zooming in with delta 0.1
  3064. // equals zooming out with a delta -0.1
  3065. var scale;
  3066. if (delta < 0) {
  3067. scale = 1 - (delta / 5);
  3068. }
  3069. else {
  3070. scale = 1 / (1 + (delta / 5)) ;
  3071. }
  3072. // calculate center, the date to zoom around
  3073. var gesture = util.fakeGesture(this, event),
  3074. pointer = getPointer(gesture.center, this.parent.frame),
  3075. pointerDate = this._pointerToDate(pointer);
  3076. this.zoom(scale, pointerDate);
  3077. }
  3078. // Prevent default actions caused by mouse wheel
  3079. // (else the page and timeline both zoom and scroll)
  3080. event.preventDefault();
  3081. };
  3082. /**
  3083. * Start of a touch gesture
  3084. * @private
  3085. */
  3086. Range.prototype._onTouch = function (event) {
  3087. touchParams.start = this.start;
  3088. touchParams.end = this.end;
  3089. touchParams.ignore = false;
  3090. touchParams.center = null;
  3091. // don't move the range when dragging a selected event
  3092. // TODO: it's not so neat to have to know about the state of the ItemSet
  3093. var item = ItemSet.itemFromTarget(event);
  3094. if (item && item.selected && this.options.editable) {
  3095. touchParams.ignore = true;
  3096. }
  3097. };
  3098. /**
  3099. * On start of a hold gesture
  3100. * @private
  3101. */
  3102. Range.prototype._onHold = function () {
  3103. touchParams.ignore = true;
  3104. };
  3105. /**
  3106. * Handle pinch event
  3107. * @param {Event} event
  3108. * @private
  3109. */
  3110. Range.prototype._onPinch = function (event) {
  3111. var direction = this.options.direction;
  3112. touchParams.ignore = true;
  3113. // TODO: reckon with option zoomable
  3114. if (event.gesture.touches.length > 1) {
  3115. if (!touchParams.center) {
  3116. touchParams.center = getPointer(event.gesture.center, this.parent.frame);
  3117. }
  3118. var scale = 1 / event.gesture.scale,
  3119. initDate = this._pointerToDate(touchParams.center),
  3120. center = getPointer(event.gesture.center, this.parent.frame),
  3121. date = this._pointerToDate(this.parent, center),
  3122. delta = date - initDate; // TODO: utilize delta
  3123. // calculate new start and end
  3124. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3125. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3126. // apply new range
  3127. this.setRange(newStart, newEnd);
  3128. }
  3129. };
  3130. /**
  3131. * Helper function to calculate the center date for zooming
  3132. * @param {{x: Number, y: Number}} pointer
  3133. * @return {number} date
  3134. * @private
  3135. */
  3136. Range.prototype._pointerToDate = function (pointer) {
  3137. var conversion;
  3138. var direction = this.options.direction;
  3139. validateDirection(direction);
  3140. if (direction == 'horizontal') {
  3141. var width = this.parent.width;
  3142. conversion = this.conversion(width);
  3143. return pointer.x / conversion.scale + conversion.offset;
  3144. }
  3145. else {
  3146. var height = this.parent.height;
  3147. conversion = this.conversion(height);
  3148. return pointer.y / conversion.scale + conversion.offset;
  3149. }
  3150. };
  3151. /**
  3152. * Get the pointer location relative to the location of the dom element
  3153. * @param {{pageX: Number, pageY: Number}} touch
  3154. * @param {Element} element HTML DOM element
  3155. * @return {{x: Number, y: Number}} pointer
  3156. * @private
  3157. */
  3158. function getPointer (touch, element) {
  3159. return {
  3160. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3161. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3162. };
  3163. }
  3164. /**
  3165. * Zoom the range the given scale in or out. Start and end date will
  3166. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3167. * date around which to zoom.
  3168. * For example, try scale = 0.9 or 1.1
  3169. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3170. * values below 1 will zoom in.
  3171. * @param {Number} [center] Value representing a date around which will
  3172. * be zoomed.
  3173. */
  3174. Range.prototype.zoom = function(scale, center) {
  3175. // if centerDate is not provided, take it half between start Date and end Date
  3176. if (center == null) {
  3177. center = (this.start + this.end) / 2;
  3178. }
  3179. // calculate new start and end
  3180. var newStart = center + (this.start - center) * scale;
  3181. var newEnd = center + (this.end - center) * scale;
  3182. this.setRange(newStart, newEnd);
  3183. };
  3184. /**
  3185. * Move the range with a given delta to the left or right. Start and end
  3186. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3187. * @param {Number} delta Moving amount. Positive value will move right,
  3188. * negative value will move left
  3189. */
  3190. Range.prototype.move = function(delta) {
  3191. // zoom start Date and end Date relative to the centerDate
  3192. var diff = (this.end - this.start);
  3193. // apply new values
  3194. var newStart = this.start + diff * delta;
  3195. var newEnd = this.end + diff * delta;
  3196. // TODO: reckon with min and max range
  3197. this.start = newStart;
  3198. this.end = newEnd;
  3199. };
  3200. /**
  3201. * Move the range to a new center point
  3202. * @param {Number} moveTo New center point of the range
  3203. */
  3204. Range.prototype.moveTo = function(moveTo) {
  3205. var center = (this.start + this.end) / 2;
  3206. var diff = center - moveTo;
  3207. // calculate new start and end
  3208. var newStart = this.start - diff;
  3209. var newEnd = this.end - diff;
  3210. this.setRange(newStart, newEnd);
  3211. };
  3212. /**
  3213. * Prototype for visual components
  3214. */
  3215. function Component () {
  3216. this.id = null;
  3217. this.parent = null;
  3218. this.childs = null;
  3219. this.options = null;
  3220. this.top = 0;
  3221. this.left = 0;
  3222. this.width = 0;
  3223. this.height = 0;
  3224. }
  3225. // Turn the Component into an event emitter
  3226. Emitter(Component.prototype);
  3227. /**
  3228. * Set parameters for the frame. Parameters will be merged in current parameter
  3229. * set.
  3230. * @param {Object} options Available parameters:
  3231. * {String | function} [className]
  3232. * {String | Number | function} [left]
  3233. * {String | Number | function} [top]
  3234. * {String | Number | function} [width]
  3235. * {String | Number | function} [height]
  3236. */
  3237. Component.prototype.setOptions = function setOptions(options) {
  3238. if (options) {
  3239. util.extend(this.options, options);
  3240. this.repaint();
  3241. }
  3242. };
  3243. /**
  3244. * Get an option value by name
  3245. * The function will first check this.options object, and else will check
  3246. * this.defaultOptions.
  3247. * @param {String} name
  3248. * @return {*} value
  3249. */
  3250. Component.prototype.getOption = function getOption(name) {
  3251. var value;
  3252. if (this.options) {
  3253. value = this.options[name];
  3254. }
  3255. if (value === undefined && this.defaultOptions) {
  3256. value = this.defaultOptions[name];
  3257. }
  3258. return value;
  3259. };
  3260. /**
  3261. * Get the frame element of the component, the outer HTML DOM element.
  3262. * @returns {HTMLElement | null} frame
  3263. */
  3264. Component.prototype.getFrame = function getFrame() {
  3265. // should be implemented by the component
  3266. return null;
  3267. };
  3268. /**
  3269. * Repaint the component
  3270. * @return {boolean} Returns true if the component is resized
  3271. */
  3272. Component.prototype.repaint = function repaint() {
  3273. // should be implemented by the component
  3274. return false;
  3275. };
  3276. /**
  3277. * Test whether the component is resized since the last time _isResized() was
  3278. * called.
  3279. * @return {Boolean} Returns true if the component is resized
  3280. * @protected
  3281. */
  3282. Component.prototype._isResized = function _isResized() {
  3283. var resized = (this._previousWidth !== this.width || this._previousHeight !== this.height);
  3284. this._previousWidth = this.width;
  3285. this._previousHeight = this.height;
  3286. return resized;
  3287. };
  3288. /**
  3289. * A panel can contain components
  3290. * @param {Object} [options] Available parameters:
  3291. * {String | Number | function} [left]
  3292. * {String | Number | function} [top]
  3293. * {String | Number | function} [width]
  3294. * {String | Number | function} [height]
  3295. * {String | function} [className]
  3296. * @constructor Panel
  3297. * @extends Component
  3298. */
  3299. function Panel(options) {
  3300. this.id = util.randomUUID();
  3301. this.parent = null;
  3302. this.childs = [];
  3303. this.options = options || {};
  3304. // create frame
  3305. this.frame = (typeof document !== 'undefined') ? document.createElement('div') : null;
  3306. }
  3307. Panel.prototype = new Component();
  3308. /**
  3309. * Set options. Will extend the current options.
  3310. * @param {Object} [options] Available parameters:
  3311. * {String | function} [className]
  3312. * {String | Number | function} [left]
  3313. * {String | Number | function} [top]
  3314. * {String | Number | function} [width]
  3315. * {String | Number | function} [height]
  3316. */
  3317. Panel.prototype.setOptions = Component.prototype.setOptions;
  3318. /**
  3319. * Get the outer frame of the panel
  3320. * @returns {HTMLElement} frame
  3321. */
  3322. Panel.prototype.getFrame = function () {
  3323. return this.frame;
  3324. };
  3325. /**
  3326. * Append a child to the panel
  3327. * @param {Component} child
  3328. */
  3329. Panel.prototype.appendChild = function (child) {
  3330. this.childs.push(child);
  3331. child.parent = this;
  3332. // attach to the DOM
  3333. var frame = child.getFrame();
  3334. if (frame) {
  3335. if (frame.parentNode) {
  3336. frame.parentNode.removeChild(frame);
  3337. }
  3338. this.frame.appendChild(frame);
  3339. }
  3340. };
  3341. /**
  3342. * Insert a child to the panel
  3343. * @param {Component} child
  3344. * @param {Component} beforeChild
  3345. */
  3346. Panel.prototype.insertBefore = function (child, beforeChild) {
  3347. var index = this.childs.indexOf(beforeChild);
  3348. if (index != -1) {
  3349. this.childs.splice(index, 0, child);
  3350. child.parent = this;
  3351. // attach to the DOM
  3352. var frame = child.getFrame();
  3353. if (frame) {
  3354. if (frame.parentNode) {
  3355. frame.parentNode.removeChild(frame);
  3356. }
  3357. var beforeFrame = beforeChild.getFrame();
  3358. if (beforeFrame) {
  3359. this.frame.insertBefore(frame, beforeFrame);
  3360. }
  3361. else {
  3362. this.frame.appendChild(frame);
  3363. }
  3364. }
  3365. }
  3366. };
  3367. /**
  3368. * Remove a child from the panel
  3369. * @param {Component} child
  3370. */
  3371. Panel.prototype.removeChild = function (child) {
  3372. var index = this.childs.indexOf(child);
  3373. if (index != -1) {
  3374. this.childs.splice(index, 1);
  3375. child.parent = null;
  3376. // remove from the DOM
  3377. var frame = child.getFrame();
  3378. if (frame && frame.parentNode) {
  3379. this.frame.removeChild(frame);
  3380. }
  3381. }
  3382. };
  3383. /**
  3384. * Test whether the panel contains given child
  3385. * @param {Component} child
  3386. */
  3387. Panel.prototype.hasChild = function (child) {
  3388. var index = this.childs.indexOf(child);
  3389. return (index != -1);
  3390. };
  3391. /**
  3392. * Repaint the component
  3393. * @return {boolean} Returns true if the component was resized since previous repaint
  3394. */
  3395. Panel.prototype.repaint = function () {
  3396. var asString = util.option.asString,
  3397. options = this.options,
  3398. frame = this.getFrame();
  3399. // update className
  3400. frame.className = 'vpanel' + (options.className ? (' ' + asString(options.className)) : '');
  3401. // repaint the child components
  3402. var childsResized = this._repaintChilds();
  3403. // update frame size
  3404. this._updateSize();
  3405. return this._isResized() || childsResized;
  3406. };
  3407. /**
  3408. * Repaint all childs of the panel
  3409. * @return {boolean} Returns true if the component is resized
  3410. * @private
  3411. */
  3412. Panel.prototype._repaintChilds = function () {
  3413. var resized = false;
  3414. for (var i = 0, ii = this.childs.length; i < ii; i++) {
  3415. resized = this.childs[i].repaint() || resized;
  3416. }
  3417. return resized;
  3418. };
  3419. /**
  3420. * Apply the size from options to the panel, and recalculate it's actual size.
  3421. * @private
  3422. */
  3423. Panel.prototype._updateSize = function () {
  3424. // apply size
  3425. this.frame.style.top = util.option.asSize(this.options.top);
  3426. this.frame.style.bottom = util.option.asSize(this.options.bottom);
  3427. this.frame.style.left = util.option.asSize(this.options.left);
  3428. this.frame.style.right = util.option.asSize(this.options.right);
  3429. this.frame.style.width = util.option.asSize(this.options.width, '100%');
  3430. this.frame.style.height = util.option.asSize(this.options.height, '');
  3431. // get actual size
  3432. this.top = this.frame.offsetTop;
  3433. this.left = this.frame.offsetLeft;
  3434. this.width = this.frame.offsetWidth;
  3435. this.height = this.frame.offsetHeight;
  3436. };
  3437. /**
  3438. * A root panel can hold components. The root panel must be initialized with
  3439. * a DOM element as container.
  3440. * @param {HTMLElement} container
  3441. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3442. * @constructor RootPanel
  3443. * @extends Panel
  3444. */
  3445. function RootPanel(container, options) {
  3446. this.id = util.randomUUID();
  3447. this.container = container;
  3448. this.options = options || {};
  3449. this.defaultOptions = {
  3450. autoResize: true
  3451. };
  3452. // create the HTML DOM
  3453. this._create();
  3454. // attach the root panel to the provided container
  3455. if (!this.container) throw new Error('Cannot repaint root panel: no container attached');
  3456. this.container.appendChild(this.getFrame());
  3457. this._initWatch();
  3458. }
  3459. RootPanel.prototype = new Panel();
  3460. /**
  3461. * Create the HTML DOM for the root panel
  3462. */
  3463. RootPanel.prototype._create = function _create() {
  3464. // create frame
  3465. this.frame = document.createElement('div');
  3466. // create event listeners for all interesting events, these events will be
  3467. // emitted via emitter
  3468. this.hammer = Hammer(this.frame, {
  3469. prevent_default: true
  3470. });
  3471. this.listeners = {};
  3472. var me = this;
  3473. var events = [
  3474. 'touch', 'pinch', 'tap', 'doubletap', 'hold',
  3475. 'dragstart', 'drag', 'dragend',
  3476. 'mousewheel', 'DOMMouseScroll' // DOMMouseScroll is for Firefox
  3477. ];
  3478. events.forEach(function (event) {
  3479. var listener = function () {
  3480. var args = [event].concat(Array.prototype.slice.call(arguments, 0));
  3481. me.emit.apply(me, args);
  3482. };
  3483. me.hammer.on(event, listener);
  3484. me.listeners[event] = listener;
  3485. });
  3486. };
  3487. /**
  3488. * Set options. Will extend the current options.
  3489. * @param {Object} [options] Available parameters:
  3490. * {String | function} [className]
  3491. * {String | Number | function} [left]
  3492. * {String | Number | function} [top]
  3493. * {String | Number | function} [width]
  3494. * {String | Number | function} [height]
  3495. * {Boolean | function} [autoResize]
  3496. */
  3497. RootPanel.prototype.setOptions = function setOptions(options) {
  3498. if (options) {
  3499. util.extend(this.options, options);
  3500. this.repaint();
  3501. this._initWatch();
  3502. }
  3503. };
  3504. /**
  3505. * Get the frame of the root panel
  3506. */
  3507. RootPanel.prototype.getFrame = function getFrame() {
  3508. return this.frame;
  3509. };
  3510. /**
  3511. * Repaint the root panel
  3512. */
  3513. RootPanel.prototype.repaint = function repaint() {
  3514. // update class name
  3515. var options = this.options;
  3516. var editable = options.editable.updateTime || options.editable.updateGroup;
  3517. var className = 'vis timeline rootpanel ' + options.orientation + (editable ? ' editable' : '');
  3518. if (options.className) className += ' ' + util.option.asString(className);
  3519. this.frame.className = className;
  3520. // repaint the child components
  3521. var childsResized = this._repaintChilds();
  3522. // update frame size
  3523. this.frame.style.maxHeight = util.option.asSize(this.options.maxHeight, '');
  3524. this.frame.style.minHeight = util.option.asSize(this.options.minHeight, '');
  3525. this._updateSize();
  3526. // if the root panel or any of its childs is resized, repaint again,
  3527. // as other components may need to be resized accordingly
  3528. var resized = this._isResized() || childsResized;
  3529. if (resized) {
  3530. setTimeout(this.repaint.bind(this), 0);
  3531. }
  3532. };
  3533. /**
  3534. * Initialize watching when option autoResize is true
  3535. * @private
  3536. */
  3537. RootPanel.prototype._initWatch = function _initWatch() {
  3538. var autoResize = this.getOption('autoResize');
  3539. if (autoResize) {
  3540. this._watch();
  3541. }
  3542. else {
  3543. this._unwatch();
  3544. }
  3545. };
  3546. /**
  3547. * Watch for changes in the size of the frame. On resize, the Panel will
  3548. * automatically redraw itself.
  3549. * @private
  3550. */
  3551. RootPanel.prototype._watch = function _watch() {
  3552. var me = this;
  3553. this._unwatch();
  3554. var checkSize = function checkSize() {
  3555. var autoResize = me.getOption('autoResize');
  3556. if (!autoResize) {
  3557. // stop watching when the option autoResize is changed to false
  3558. me._unwatch();
  3559. return;
  3560. }
  3561. if (me.frame) {
  3562. // check whether the frame is resized
  3563. if ((me.frame.clientWidth != me.lastWidth) ||
  3564. (me.frame.clientHeight != me.lastHeight)) {
  3565. me.lastWidth = me.frame.clientWidth;
  3566. me.lastHeight = me.frame.clientHeight;
  3567. me.repaint();
  3568. // TODO: emit a resize event instead?
  3569. }
  3570. }
  3571. };
  3572. // TODO: automatically cleanup the event listener when the frame is deleted
  3573. util.addEventListener(window, 'resize', checkSize);
  3574. this.watchTimer = setInterval(checkSize, 1000);
  3575. };
  3576. /**
  3577. * Stop watching for a resize of the frame.
  3578. * @private
  3579. */
  3580. RootPanel.prototype._unwatch = function _unwatch() {
  3581. if (this.watchTimer) {
  3582. clearInterval(this.watchTimer);
  3583. this.watchTimer = undefined;
  3584. }
  3585. // TODO: remove event listener on window.resize
  3586. };
  3587. /**
  3588. * A horizontal time axis
  3589. * @param {Object} [options] See TimeAxis.setOptions for the available
  3590. * options.
  3591. * @constructor TimeAxis
  3592. * @extends Component
  3593. */
  3594. function TimeAxis (options) {
  3595. this.id = util.randomUUID();
  3596. this.dom = {
  3597. majorLines: [],
  3598. majorTexts: [],
  3599. minorLines: [],
  3600. minorTexts: [],
  3601. redundant: {
  3602. majorLines: [],
  3603. majorTexts: [],
  3604. minorLines: [],
  3605. minorTexts: []
  3606. }
  3607. };
  3608. this.props = {
  3609. range: {
  3610. start: 0,
  3611. end: 0,
  3612. minimumStep: 0
  3613. },
  3614. lineTop: 0
  3615. };
  3616. this.options = options || {};
  3617. this.defaultOptions = {
  3618. orientation: 'bottom', // supported: 'top', 'bottom'
  3619. // TODO: implement timeaxis orientations 'left' and 'right'
  3620. showMinorLabels: true,
  3621. showMajorLabels: true
  3622. };
  3623. this.range = null;
  3624. // create the HTML DOM
  3625. this._create();
  3626. }
  3627. TimeAxis.prototype = new Component();
  3628. // TODO: comment options
  3629. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3630. /**
  3631. * Create the HTML DOM for the TimeAxis
  3632. */
  3633. TimeAxis.prototype._create = function _create() {
  3634. this.frame = document.createElement('div');
  3635. };
  3636. /**
  3637. * Set a range (start and end)
  3638. * @param {Range | Object} range A Range or an object containing start and end.
  3639. */
  3640. TimeAxis.prototype.setRange = function (range) {
  3641. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3642. throw new TypeError('Range must be an instance of Range, ' +
  3643. 'or an object containing start and end.');
  3644. }
  3645. this.range = range;
  3646. };
  3647. /**
  3648. * Get the outer frame of the time axis
  3649. * @return {HTMLElement} frame
  3650. */
  3651. TimeAxis.prototype.getFrame = function getFrame() {
  3652. return this.frame;
  3653. };
  3654. /**
  3655. * Repaint the component
  3656. * @return {boolean} Returns true if the component is resized
  3657. */
  3658. TimeAxis.prototype.repaint = function () {
  3659. var asSize = util.option.asSize,
  3660. options = this.options,
  3661. props = this.props,
  3662. frame = this.frame;
  3663. // update classname
  3664. frame.className = 'timeaxis'; // TODO: add className from options if defined
  3665. var parent = frame.parentNode;
  3666. if (parent) {
  3667. // calculate character width and height
  3668. this._calculateCharSize();
  3669. // TODO: recalculate sizes only needed when parent is resized or options is changed
  3670. var orientation = this.getOption('orientation'),
  3671. showMinorLabels = this.getOption('showMinorLabels'),
  3672. showMajorLabels = this.getOption('showMajorLabels');
  3673. // determine the width and height of the elemens for the axis
  3674. var parentHeight = this.parent.height;
  3675. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  3676. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  3677. this.height = props.minorLabelHeight + props.majorLabelHeight;
  3678. this.width = frame.offsetWidth; // TODO: only update the width when the frame is resized?
  3679. props.minorLineHeight = parentHeight + props.minorLabelHeight;
  3680. props.minorLineWidth = 1; // TODO: really calculate width
  3681. props.majorLineHeight = parentHeight + this.height;
  3682. props.majorLineWidth = 1; // TODO: really calculate width
  3683. // take frame offline while updating (is almost twice as fast)
  3684. var beforeChild = frame.nextSibling;
  3685. parent.removeChild(frame);
  3686. // TODO: top/bottom positioning should be determined by options set in the Timeline, not here
  3687. if (orientation == 'top') {
  3688. frame.style.top = '0';
  3689. frame.style.left = '0';
  3690. frame.style.bottom = '';
  3691. frame.style.width = asSize(options.width, '100%');
  3692. frame.style.height = this.height + 'px';
  3693. }
  3694. else { // bottom
  3695. frame.style.top = '';
  3696. frame.style.bottom = '0';
  3697. frame.style.left = '0';
  3698. frame.style.width = asSize(options.width, '100%');
  3699. frame.style.height = this.height + 'px';
  3700. }
  3701. this._repaintLabels();
  3702. this._repaintLine();
  3703. // put frame online again
  3704. if (beforeChild) {
  3705. parent.insertBefore(frame, beforeChild);
  3706. }
  3707. else {
  3708. parent.appendChild(frame)
  3709. }
  3710. }
  3711. return this._isResized();
  3712. };
  3713. /**
  3714. * Repaint major and minor text labels and vertical grid lines
  3715. * @private
  3716. */
  3717. TimeAxis.prototype._repaintLabels = function () {
  3718. var orientation = this.getOption('orientation');
  3719. // calculate range and step (step such that we have space for 7 characters per label)
  3720. var start = util.convert(this.range.start, 'Number'),
  3721. end = util.convert(this.range.end, 'Number'),
  3722. minimumStep = this.options.toTime((this.props.minorCharWidth || 10) * 7).valueOf()
  3723. -this.options.toTime(0).valueOf();
  3724. var step = new TimeStep(new Date(start), new Date(end), minimumStep);
  3725. this.step = step;
  3726. // Move all DOM elements to a "redundant" list, where they
  3727. // can be picked for re-use, and clear the lists with lines and texts.
  3728. // At the end of the function _repaintLabels, left over elements will be cleaned up
  3729. var dom = this.dom;
  3730. dom.redundant.majorLines = dom.majorLines;
  3731. dom.redundant.majorTexts = dom.majorTexts;
  3732. dom.redundant.minorLines = dom.minorLines;
  3733. dom.redundant.minorTexts = dom.minorTexts;
  3734. dom.majorLines = [];
  3735. dom.majorTexts = [];
  3736. dom.minorLines = [];
  3737. dom.minorTexts = [];
  3738. step.first();
  3739. var xFirstMajorLabel = undefined;
  3740. var max = 0;
  3741. while (step.hasNext() && max < 1000) {
  3742. max++;
  3743. var cur = step.getCurrent(),
  3744. x = this.options.toScreen(cur),
  3745. isMajor = step.isMajor();
  3746. // TODO: lines must have a width, such that we can create css backgrounds
  3747. if (this.getOption('showMinorLabels')) {
  3748. this._repaintMinorText(x, step.getLabelMinor(), orientation);
  3749. }
  3750. if (isMajor && this.getOption('showMajorLabels')) {
  3751. if (x > 0) {
  3752. if (xFirstMajorLabel == undefined) {
  3753. xFirstMajorLabel = x;
  3754. }
  3755. this._repaintMajorText(x, step.getLabelMajor(), orientation);
  3756. }
  3757. this._repaintMajorLine(x, orientation);
  3758. }
  3759. else {
  3760. this._repaintMinorLine(x, orientation);
  3761. }
  3762. step.next();
  3763. }
  3764. // create a major label on the left when needed
  3765. if (this.getOption('showMajorLabels')) {
  3766. var leftTime = this.options.toTime(0),
  3767. leftText = step.getLabelMajor(leftTime),
  3768. widthText = leftText.length * (this.props.majorCharWidth || 10) + 10; // upper bound estimation
  3769. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3770. this._repaintMajorText(0, leftText, orientation);
  3771. }
  3772. }
  3773. // Cleanup leftover DOM elements from the redundant list
  3774. util.forEach(this.dom.redundant, function (arr) {
  3775. while (arr.length) {
  3776. var elem = arr.pop();
  3777. if (elem && elem.parentNode) {
  3778. elem.parentNode.removeChild(elem);
  3779. }
  3780. }
  3781. });
  3782. };
  3783. /**
  3784. * Create a minor label for the axis at position x
  3785. * @param {Number} x
  3786. * @param {String} text
  3787. * @param {String} orientation "top" or "bottom" (default)
  3788. * @private
  3789. */
  3790. TimeAxis.prototype._repaintMinorText = function (x, text, orientation) {
  3791. // reuse redundant label
  3792. var label = this.dom.redundant.minorTexts.shift();
  3793. if (!label) {
  3794. // create new label
  3795. var content = document.createTextNode('');
  3796. label = document.createElement('div');
  3797. label.appendChild(content);
  3798. label.className = 'text minor';
  3799. this.frame.appendChild(label);
  3800. }
  3801. this.dom.minorTexts.push(label);
  3802. label.childNodes[0].nodeValue = text;
  3803. if (orientation == 'top') {
  3804. label.style.top = this.props.majorLabelHeight + 'px';
  3805. label.style.bottom = '';
  3806. }
  3807. else {
  3808. label.style.top = '';
  3809. label.style.bottom = this.props.majorLabelHeight + 'px';
  3810. }
  3811. label.style.left = x + 'px';
  3812. //label.title = title; // TODO: this is a heavy operation
  3813. };
  3814. /**
  3815. * Create a Major label for the axis at position x
  3816. * @param {Number} x
  3817. * @param {String} text
  3818. * @param {String} orientation "top" or "bottom" (default)
  3819. * @private
  3820. */
  3821. TimeAxis.prototype._repaintMajorText = function (x, text, orientation) {
  3822. // reuse redundant label
  3823. var label = this.dom.redundant.majorTexts.shift();
  3824. if (!label) {
  3825. // create label
  3826. var content = document.createTextNode(text);
  3827. label = document.createElement('div');
  3828. label.className = 'text major';
  3829. label.appendChild(content);
  3830. this.frame.appendChild(label);
  3831. }
  3832. this.dom.majorTexts.push(label);
  3833. label.childNodes[0].nodeValue = text;
  3834. //label.title = title; // TODO: this is a heavy operation
  3835. if (orientation == 'top') {
  3836. label.style.top = '0px';
  3837. label.style.bottom = '';
  3838. }
  3839. else {
  3840. label.style.top = '';
  3841. label.style.bottom = '0px';
  3842. }
  3843. label.style.left = x + 'px';
  3844. };
  3845. /**
  3846. * Create a minor line for the axis at position x
  3847. * @param {Number} x
  3848. * @param {String} orientation "top" or "bottom" (default)
  3849. * @private
  3850. */
  3851. TimeAxis.prototype._repaintMinorLine = function (x, orientation) {
  3852. // reuse redundant line
  3853. var line = this.dom.redundant.minorLines.shift();
  3854. if (!line) {
  3855. // create vertical line
  3856. line = document.createElement('div');
  3857. line.className = 'grid vertical minor';
  3858. this.frame.appendChild(line);
  3859. }
  3860. this.dom.minorLines.push(line);
  3861. var props = this.props;
  3862. if (orientation == 'top') {
  3863. line.style.top = this.props.majorLabelHeight + 'px';
  3864. line.style.bottom = '';
  3865. }
  3866. else {
  3867. line.style.top = '';
  3868. line.style.bottom = this.props.majorLabelHeight + 'px';
  3869. }
  3870. line.style.height = props.minorLineHeight + 'px';
  3871. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  3872. };
  3873. /**
  3874. * Create a Major line for the axis at position x
  3875. * @param {Number} x
  3876. * @param {String} orientation "top" or "bottom" (default)
  3877. * @private
  3878. */
  3879. TimeAxis.prototype._repaintMajorLine = function (x, orientation) {
  3880. // reuse redundant line
  3881. var line = this.dom.redundant.majorLines.shift();
  3882. if (!line) {
  3883. // create vertical line
  3884. line = document.createElement('DIV');
  3885. line.className = 'grid vertical major';
  3886. this.frame.appendChild(line);
  3887. }
  3888. this.dom.majorLines.push(line);
  3889. var props = this.props;
  3890. if (orientation == 'top') {
  3891. line.style.top = '0px';
  3892. line.style.bottom = '';
  3893. }
  3894. else {
  3895. line.style.top = '';
  3896. line.style.bottom = '0px';
  3897. }
  3898. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  3899. line.style.height = props.majorLineHeight + 'px';
  3900. };
  3901. /**
  3902. * Repaint the horizontal line for the axis
  3903. * @private
  3904. */
  3905. TimeAxis.prototype._repaintLine = function() {
  3906. var line = this.dom.line,
  3907. frame = this.frame,
  3908. orientation = this.getOption('orientation');
  3909. // line before all axis elements
  3910. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  3911. if (line) {
  3912. // put this line at the end of all childs
  3913. frame.removeChild(line);
  3914. frame.appendChild(line);
  3915. }
  3916. else {
  3917. // create the axis line
  3918. line = document.createElement('div');
  3919. line.className = 'grid horizontal major';
  3920. frame.appendChild(line);
  3921. this.dom.line = line;
  3922. }
  3923. if (orientation == 'top') {
  3924. line.style.top = this.height + 'px';
  3925. line.style.bottom = '';
  3926. }
  3927. else {
  3928. line.style.top = '';
  3929. line.style.bottom = this.height + 'px';
  3930. }
  3931. }
  3932. else {
  3933. if (line && line.parentNode) {
  3934. line.parentNode.removeChild(line);
  3935. delete this.dom.line;
  3936. }
  3937. }
  3938. };
  3939. /**
  3940. * Determine the size of text on the axis (both major and minor axis).
  3941. * The size is calculated only once and then cached in this.props.
  3942. * @private
  3943. */
  3944. TimeAxis.prototype._calculateCharSize = function () {
  3945. // Note: We calculate char size with every repaint. Size may change, for
  3946. // example when any of the timelines parents had display:none for example.
  3947. // determine the char width and height on the minor axis
  3948. if (!this.dom.measureCharMinor) {
  3949. this.dom.measureCharMinor = document.createElement('DIV');
  3950. this.dom.measureCharMinor.className = 'text minor measure';
  3951. this.dom.measureCharMinor.style.position = 'absolute';
  3952. this.dom.measureCharMinor.appendChild(document.createTextNode('0'));
  3953. this.frame.appendChild(this.dom.measureCharMinor);
  3954. }
  3955. this.props.minorCharHeight = this.dom.measureCharMinor.clientHeight;
  3956. this.props.minorCharWidth = this.dom.measureCharMinor.clientWidth;
  3957. // determine the char width and height on the major axis
  3958. if (!this.dom.measureCharMajor) {
  3959. this.dom.measureCharMajor = document.createElement('DIV');
  3960. this.dom.measureCharMajor.className = 'text minor measure';
  3961. this.dom.measureCharMajor.style.position = 'absolute';
  3962. this.dom.measureCharMajor.appendChild(document.createTextNode('0'));
  3963. this.frame.appendChild(this.dom.measureCharMajor);
  3964. }
  3965. this.props.majorCharHeight = this.dom.measureCharMajor.clientHeight;
  3966. this.props.majorCharWidth = this.dom.measureCharMajor.clientWidth;
  3967. };
  3968. /**
  3969. * Snap a date to a rounded value.
  3970. * The snap intervals are dependent on the current scale and step.
  3971. * @param {Date} date the date to be snapped.
  3972. * @return {Date} snappedDate
  3973. */
  3974. TimeAxis.prototype.snap = function snap (date) {
  3975. return this.step.snap(date);
  3976. };
  3977. /**
  3978. * A current time bar
  3979. * @param {Range} range
  3980. * @param {Object} [options] Available parameters:
  3981. * {Boolean} [showCurrentTime]
  3982. * @constructor CurrentTime
  3983. * @extends Component
  3984. */
  3985. function CurrentTime (range, options) {
  3986. this.id = util.randomUUID();
  3987. this.range = range;
  3988. this.options = options || {};
  3989. this.defaultOptions = {
  3990. showCurrentTime: false
  3991. };
  3992. this._create();
  3993. }
  3994. CurrentTime.prototype = new Component();
  3995. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  3996. /**
  3997. * Create the HTML DOM for the current time bar
  3998. * @private
  3999. */
  4000. CurrentTime.prototype._create = function _create () {
  4001. var bar = document.createElement('div');
  4002. bar.className = 'currenttime';
  4003. bar.style.position = 'absolute';
  4004. bar.style.top = '0px';
  4005. bar.style.height = '100%';
  4006. this.bar = bar;
  4007. };
  4008. /**
  4009. * Get the frame element of the current time bar
  4010. * @returns {HTMLElement} frame
  4011. */
  4012. CurrentTime.prototype.getFrame = function getFrame() {
  4013. return this.bar;
  4014. };
  4015. /**
  4016. * Repaint the component
  4017. * @return {boolean} Returns true if the component is resized
  4018. */
  4019. CurrentTime.prototype.repaint = function repaint() {
  4020. var parent = this.parent;
  4021. var now = new Date();
  4022. var x = this.options.toScreen(now);
  4023. this.bar.style.left = x + 'px';
  4024. this.bar.title = 'Current time: ' + now;
  4025. return false;
  4026. };
  4027. /**
  4028. * Start auto refreshing the current time bar
  4029. */
  4030. CurrentTime.prototype.start = function start() {
  4031. var me = this;
  4032. function update () {
  4033. me.stop();
  4034. // determine interval to refresh
  4035. var scale = me.range.conversion(me.parent.width).scale;
  4036. var interval = 1 / scale / 10;
  4037. if (interval < 30) interval = 30;
  4038. if (interval > 1000) interval = 1000;
  4039. me.repaint();
  4040. // start a timer to adjust for the new time
  4041. me.currentTimeTimer = setTimeout(update, interval);
  4042. }
  4043. update();
  4044. };
  4045. /**
  4046. * Stop auto refreshing the current time bar
  4047. */
  4048. CurrentTime.prototype.stop = function stop() {
  4049. if (this.currentTimeTimer !== undefined) {
  4050. clearTimeout(this.currentTimeTimer);
  4051. delete this.currentTimeTimer;
  4052. }
  4053. };
  4054. /**
  4055. * A custom time bar
  4056. * @param {Object} [options] Available parameters:
  4057. * {Boolean} [showCustomTime]
  4058. * @constructor CustomTime
  4059. * @extends Component
  4060. */
  4061. function CustomTime (options) {
  4062. this.id = util.randomUUID();
  4063. this.options = options || {};
  4064. this.defaultOptions = {
  4065. showCustomTime: false
  4066. };
  4067. this.customTime = new Date();
  4068. this.eventParams = {}; // stores state parameters while dragging the bar
  4069. // create the DOM
  4070. this._create();
  4071. }
  4072. CustomTime.prototype = new Component();
  4073. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4074. /**
  4075. * Create the DOM for the custom time
  4076. * @private
  4077. */
  4078. CustomTime.prototype._create = function _create () {
  4079. var bar = document.createElement('div');
  4080. bar.className = 'customtime';
  4081. bar.style.position = 'absolute';
  4082. bar.style.top = '0px';
  4083. bar.style.height = '100%';
  4084. this.bar = bar;
  4085. var drag = document.createElement('div');
  4086. drag.style.position = 'relative';
  4087. drag.style.top = '0px';
  4088. drag.style.left = '-10px';
  4089. drag.style.height = '100%';
  4090. drag.style.width = '20px';
  4091. bar.appendChild(drag);
  4092. // attach event listeners
  4093. this.hammer = Hammer(bar, {
  4094. prevent_default: true
  4095. });
  4096. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4097. this.hammer.on('drag', this._onDrag.bind(this));
  4098. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4099. };
  4100. /**
  4101. * Get the frame element of the custom time bar
  4102. * @returns {HTMLElement} frame
  4103. */
  4104. CustomTime.prototype.getFrame = function getFrame() {
  4105. return this.bar;
  4106. };
  4107. /**
  4108. * Repaint the component
  4109. * @return {boolean} Returns true if the component is resized
  4110. */
  4111. CustomTime.prototype.repaint = function () {
  4112. var x = this.options.toScreen(this.customTime);
  4113. this.bar.style.left = x + 'px';
  4114. this.bar.title = 'Time: ' + this.customTime;
  4115. return false;
  4116. };
  4117. /**
  4118. * Set custom time.
  4119. * @param {Date} time
  4120. */
  4121. CustomTime.prototype.setCustomTime = function(time) {
  4122. this.customTime = new Date(time.valueOf());
  4123. this.repaint();
  4124. };
  4125. /**
  4126. * Retrieve the current custom time.
  4127. * @return {Date} customTime
  4128. */
  4129. CustomTime.prototype.getCustomTime = function() {
  4130. return new Date(this.customTime.valueOf());
  4131. };
  4132. /**
  4133. * Start moving horizontally
  4134. * @param {Event} event
  4135. * @private
  4136. */
  4137. CustomTime.prototype._onDragStart = function(event) {
  4138. this.eventParams.dragging = true;
  4139. this.eventParams.customTime = this.customTime;
  4140. event.stopPropagation();
  4141. event.preventDefault();
  4142. };
  4143. /**
  4144. * Perform moving operating.
  4145. * @param {Event} event
  4146. * @private
  4147. */
  4148. CustomTime.prototype._onDrag = function (event) {
  4149. if (!this.eventParams.dragging) return;
  4150. var deltaX = event.gesture.deltaX,
  4151. x = this.options.toScreen(this.eventParams.customTime) + deltaX,
  4152. time = this.options.toTime(x);
  4153. this.setCustomTime(time);
  4154. // fire a timechange event
  4155. this.emit('timechange', {
  4156. time: new Date(this.customTime.valueOf())
  4157. });
  4158. event.stopPropagation();
  4159. event.preventDefault();
  4160. };
  4161. /**
  4162. * Stop moving operating.
  4163. * @param {event} event
  4164. * @private
  4165. */
  4166. CustomTime.prototype._onDragEnd = function (event) {
  4167. if (!this.eventParams.dragging) return;
  4168. // fire a timechanged event
  4169. this.emit('timechanged', {
  4170. time: new Date(this.customTime.valueOf())
  4171. });
  4172. event.stopPropagation();
  4173. event.preventDefault();
  4174. };
  4175. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  4176. /**
  4177. * An ItemSet holds a set of items and ranges which can be displayed in a
  4178. * range. The width is determined by the parent of the ItemSet, and the height
  4179. * is determined by the size of the items.
  4180. * @param {Panel} backgroundPanel Panel which can be used to display the
  4181. * vertical lines of box items.
  4182. * @param {Panel} axisPanel Panel on the axis where the dots of box-items
  4183. * can be displayed.
  4184. * @param {Panel} sidePanel Left side panel holding labels
  4185. * @param {Object} [options] See ItemSet.setOptions for the available options.
  4186. * @constructor ItemSet
  4187. * @extends Panel
  4188. */
  4189. function ItemSet(backgroundPanel, axisPanel, sidePanel, options) {
  4190. this.id = util.randomUUID();
  4191. // one options object is shared by this itemset and all its items
  4192. this.options = options || {};
  4193. this.backgroundPanel = backgroundPanel;
  4194. this.axisPanel = axisPanel;
  4195. this.sidePanel = sidePanel;
  4196. this.itemOptions = Object.create(this.options);
  4197. this.dom = {};
  4198. this.hammer = null;
  4199. var me = this;
  4200. this.itemsData = null; // DataSet
  4201. this.groupsData = null; // DataSet
  4202. this.range = null; // Range or Object {start: number, end: number}
  4203. // listeners for the DataSet of the items
  4204. this.itemListeners = {
  4205. 'add': function (event, params, senderId) {
  4206. if (senderId != me.id) me._onAdd(params.items);
  4207. },
  4208. 'update': function (event, params, senderId) {
  4209. if (senderId != me.id) me._onUpdate(params.items);
  4210. },
  4211. 'remove': function (event, params, senderId) {
  4212. if (senderId != me.id) me._onRemove(params.items);
  4213. }
  4214. };
  4215. // listeners for the DataSet of the groups
  4216. this.groupListeners = {
  4217. 'add': function (event, params, senderId) {
  4218. if (senderId != me.id) me._onAddGroups(params.items);
  4219. },
  4220. 'update': function (event, params, senderId) {
  4221. if (senderId != me.id) me._onUpdateGroups(params.items);
  4222. },
  4223. 'remove': function (event, params, senderId) {
  4224. if (senderId != me.id) me._onRemoveGroups(params.items);
  4225. }
  4226. };
  4227. this.items = {}; // object with an Item for every data item
  4228. this.groups = {}; // Group object for every group
  4229. this.groupIds = [];
  4230. this.selection = []; // list with the ids of all selected nodes
  4231. this.stackDirty = true; // if true, all items will be restacked on next repaint
  4232. this.touchParams = {}; // stores properties while dragging
  4233. // create the HTML DOM
  4234. this._create();
  4235. }
  4236. ItemSet.prototype = new Panel();
  4237. // available item types will be registered here
  4238. ItemSet.types = {
  4239. box: ItemBox,
  4240. range: ItemRange,
  4241. rangeoverflow: ItemRangeOverflow,
  4242. point: ItemPoint
  4243. };
  4244. /**
  4245. * Create the HTML DOM for the ItemSet
  4246. */
  4247. ItemSet.prototype._create = function _create(){
  4248. var frame = document.createElement('div');
  4249. frame['timeline-itemset'] = this;
  4250. this.frame = frame;
  4251. // create background panel
  4252. var background = document.createElement('div');
  4253. background.className = 'background';
  4254. this.backgroundPanel.frame.appendChild(background);
  4255. this.dom.background = background;
  4256. // create foreground panel
  4257. var foreground = document.createElement('div');
  4258. foreground.className = 'foreground';
  4259. frame.appendChild(foreground);
  4260. this.dom.foreground = foreground;
  4261. // create axis panel
  4262. var axis = document.createElement('div');
  4263. axis.className = 'axis';
  4264. this.dom.axis = axis;
  4265. this.axisPanel.frame.appendChild(axis);
  4266. // create labelset
  4267. var labelSet = document.createElement('div');
  4268. labelSet.className = 'labelset';
  4269. this.dom.labelSet = labelSet;
  4270. this.sidePanel.frame.appendChild(labelSet);
  4271. // create ungrouped Group
  4272. this._updateUngrouped();
  4273. // attach event listeners
  4274. // TODO: use event listeners from the rootpanel to improve performance?
  4275. this.hammer = Hammer(frame, {
  4276. prevent_default: true
  4277. });
  4278. this.hammer.on('dragstart', this._onDragStart.bind(this));
  4279. this.hammer.on('drag', this._onDrag.bind(this));
  4280. this.hammer.on('dragend', this._onDragEnd.bind(this));
  4281. };
  4282. /**
  4283. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4284. * @param {Object} [options] The following options are available:
  4285. * {String | function} [className]
  4286. * class name for the itemset
  4287. * {String} [type]
  4288. * Default type for the items. Choose from 'box'
  4289. * (default), 'point', or 'range'. The default
  4290. * Style can be overwritten by individual items.
  4291. * {String} align
  4292. * Alignment for the items, only applicable for
  4293. * ItemBox. Choose 'center' (default), 'left', or
  4294. * 'right'.
  4295. * {String} orientation
  4296. * Orientation of the item set. Choose 'top' or
  4297. * 'bottom' (default).
  4298. * {Number} margin.axis
  4299. * Margin between the axis and the items in pixels.
  4300. * Default is 20.
  4301. * {Number} margin.item
  4302. * Margin between items in pixels. Default is 10.
  4303. * {Number} padding
  4304. * Padding of the contents of an item in pixels.
  4305. * Must correspond with the items css. Default is 5.
  4306. * {Function} snap
  4307. * Function to let items snap to nice dates when
  4308. * dragging items.
  4309. */
  4310. ItemSet.prototype.setOptions = function setOptions(options) {
  4311. Component.prototype.setOptions.call(this, options);
  4312. };
  4313. /**
  4314. * Mark the ItemSet dirty so it will refresh everything with next repaint
  4315. */
  4316. ItemSet.prototype.markDirty = function markDirty() {
  4317. this.groupIds = [];
  4318. this.stackDirty = true;
  4319. };
  4320. /**
  4321. * Hide the component from the DOM
  4322. */
  4323. ItemSet.prototype.hide = function hide() {
  4324. // remove the axis with dots
  4325. if (this.dom.axis.parentNode) {
  4326. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4327. }
  4328. // remove the background with vertical lines
  4329. if (this.dom.background.parentNode) {
  4330. this.dom.background.parentNode.removeChild(this.dom.background);
  4331. }
  4332. // remove the labelset containing all group labels
  4333. if (this.dom.labelSet.parentNode) {
  4334. this.dom.labelSet.parentNode.removeChild(this.dom.labelSet);
  4335. }
  4336. };
  4337. /**
  4338. * Show the component in the DOM (when not already visible).
  4339. * @return {Boolean} changed
  4340. */
  4341. ItemSet.prototype.show = function show() {
  4342. // show axis with dots
  4343. if (!this.dom.axis.parentNode) {
  4344. this.axisPanel.frame.appendChild(this.dom.axis);
  4345. }
  4346. // show background with vertical lines
  4347. if (!this.dom.background.parentNode) {
  4348. this.backgroundPanel.frame.appendChild(this.dom.background);
  4349. }
  4350. // show labelset containing labels
  4351. if (!this.dom.labelSet.parentNode) {
  4352. this.sidePanel.frame.appendChild(this.dom.labelSet);
  4353. }
  4354. };
  4355. /**
  4356. * Set range (start and end).
  4357. * @param {Range | Object} range A Range or an object containing start and end.
  4358. */
  4359. ItemSet.prototype.setRange = function setRange(range) {
  4360. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4361. throw new TypeError('Range must be an instance of Range, ' +
  4362. 'or an object containing start and end.');
  4363. }
  4364. this.range = range;
  4365. };
  4366. /**
  4367. * Set selected items by their id. Replaces the current selection
  4368. * Unknown id's are silently ignored.
  4369. * @param {Array} [ids] An array with zero or more id's of the items to be
  4370. * selected. If ids is an empty array, all items will be
  4371. * unselected.
  4372. */
  4373. ItemSet.prototype.setSelection = function setSelection(ids) {
  4374. var i, ii, id, item;
  4375. if (ids) {
  4376. if (!Array.isArray(ids)) {
  4377. throw new TypeError('Array expected');
  4378. }
  4379. // unselect currently selected items
  4380. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4381. id = this.selection[i];
  4382. item = this.items[id];
  4383. if (item) item.unselect();
  4384. }
  4385. // select items
  4386. this.selection = [];
  4387. for (i = 0, ii = ids.length; i < ii; i++) {
  4388. id = ids[i];
  4389. item = this.items[id];
  4390. if (item) {
  4391. this.selection.push(id);
  4392. item.select();
  4393. }
  4394. }
  4395. }
  4396. };
  4397. /**
  4398. * Get the selected items by their id
  4399. * @return {Array} ids The ids of the selected items
  4400. */
  4401. ItemSet.prototype.getSelection = function getSelection() {
  4402. return this.selection.concat([]);
  4403. };
  4404. /**
  4405. * Deselect a selected item
  4406. * @param {String | Number} id
  4407. * @private
  4408. */
  4409. ItemSet.prototype._deselect = function _deselect(id) {
  4410. var selection = this.selection;
  4411. for (var i = 0, ii = selection.length; i < ii; i++) {
  4412. if (selection[i] == id) { // non-strict comparison!
  4413. selection.splice(i, 1);
  4414. break;
  4415. }
  4416. }
  4417. };
  4418. /**
  4419. * Return the item sets frame
  4420. * @returns {HTMLElement} frame
  4421. */
  4422. ItemSet.prototype.getFrame = function getFrame() {
  4423. return this.frame;
  4424. };
  4425. /**
  4426. * Repaint the component
  4427. * @return {boolean} Returns true if the component is resized
  4428. */
  4429. ItemSet.prototype.repaint = function repaint() {
  4430. var margin = this.options.margin,
  4431. range = this.range,
  4432. asSize = util.option.asSize,
  4433. asString = util.option.asString,
  4434. options = this.options,
  4435. orientation = this.getOption('orientation'),
  4436. resized = false,
  4437. frame = this.frame;
  4438. // TODO: document this feature to specify one margin for both item and axis distance
  4439. if (typeof margin === 'number') {
  4440. margin = {
  4441. item: margin,
  4442. axis: margin
  4443. };
  4444. }
  4445. // update className
  4446. frame.className = 'itemset' + (options.className ? (' ' + asString(options.className)) : '');
  4447. // reorder the groups (if needed)
  4448. resized = this._orderGroups() || resized;
  4449. // check whether zoomed (in that case we need to re-stack everything)
  4450. // TODO: would be nicer to get this as a trigger from Range
  4451. var visibleInterval = this.range.end - this.range.start;
  4452. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  4453. if (zoomed) this.stackDirty = true;
  4454. this.lastVisibleInterval = visibleInterval;
  4455. this.lastWidth = this.width;
  4456. // repaint all groups
  4457. var restack = this.stackDirty,
  4458. firstGroup = this._firstGroup(),
  4459. firstMargin = {
  4460. item: margin.item,
  4461. axis: margin.axis
  4462. },
  4463. nonFirstMargin = {
  4464. item: margin.item,
  4465. axis: margin.item / 2
  4466. },
  4467. height = 0,
  4468. minHeight = margin.axis + margin.item;
  4469. util.forEach(this.groups, function (group) {
  4470. var groupMargin = (group == firstGroup) ? firstMargin : nonFirstMargin;
  4471. resized = group.repaint(range, groupMargin, restack) || resized;
  4472. height += group.height;
  4473. });
  4474. height = Math.max(height, minHeight);
  4475. this.stackDirty = false;
  4476. // reposition frame
  4477. frame.style.left = asSize(options.left, '');
  4478. frame.style.right = asSize(options.right, '');
  4479. frame.style.top = asSize((orientation == 'top') ? '0' : '');
  4480. frame.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4481. frame.style.width = asSize(options.width, '100%');
  4482. frame.style.height = asSize(height);
  4483. //frame.style.height = asSize('height' in options ? options.height : height); // TODO: reckon with height
  4484. // calculate actual size and position
  4485. this.top = frame.offsetTop;
  4486. this.left = frame.offsetLeft;
  4487. this.width = frame.offsetWidth;
  4488. this.height = height;
  4489. // reposition axis
  4490. this.dom.axis.style.left = asSize(options.left, '0');
  4491. this.dom.axis.style.right = asSize(options.right, '');
  4492. this.dom.axis.style.width = asSize(options.width, '100%');
  4493. this.dom.axis.style.height = asSize(0);
  4494. this.dom.axis.style.top = asSize((orientation == 'top') ? '0' : '');
  4495. this.dom.axis.style.bottom = asSize((orientation == 'top') ? '' : '0');
  4496. // check if this component is resized
  4497. resized = this._isResized() || resized;
  4498. return resized;
  4499. };
  4500. /**
  4501. * Get the first group, aligned with the axis
  4502. * @return {Group | null} firstGroup
  4503. * @private
  4504. */
  4505. ItemSet.prototype._firstGroup = function _firstGroup() {
  4506. var firstGroupIndex = (this.options.orientation == 'top') ? 0 : (this.groupIds.length - 1);
  4507. var firstGroupId = this.groupIds[firstGroupIndex];
  4508. var firstGroup = this.groups[firstGroupId] || this.groups[UNGROUPED];
  4509. return firstGroup || null;
  4510. };
  4511. /**
  4512. * Create or delete the group holding all ungrouped items. This group is used when
  4513. * there are no groups specified.
  4514. * @protected
  4515. */
  4516. ItemSet.prototype._updateUngrouped = function _updateUngrouped() {
  4517. var ungrouped = this.groups[UNGROUPED];
  4518. if (this.groupsData) {
  4519. // remove the group holding all ungrouped items
  4520. if (ungrouped) {
  4521. ungrouped.hide();
  4522. delete this.groups[UNGROUPED];
  4523. }
  4524. }
  4525. else {
  4526. // create a group holding all (unfiltered) items
  4527. if (!ungrouped) {
  4528. var id = null;
  4529. var data = null;
  4530. ungrouped = new Group(id, data, this);
  4531. this.groups[UNGROUPED] = ungrouped;
  4532. for (var itemId in this.items) {
  4533. if (this.items.hasOwnProperty(itemId)) {
  4534. ungrouped.add(this.items[itemId]);
  4535. }
  4536. }
  4537. ungrouped.show();
  4538. }
  4539. }
  4540. };
  4541. /**
  4542. * Get the foreground container element
  4543. * @return {HTMLElement} foreground
  4544. */
  4545. ItemSet.prototype.getForeground = function getForeground() {
  4546. return this.dom.foreground;
  4547. };
  4548. /**
  4549. * Get the background container element
  4550. * @return {HTMLElement} background
  4551. */
  4552. ItemSet.prototype.getBackground = function getBackground() {
  4553. return this.dom.background;
  4554. };
  4555. /**
  4556. * Get the axis container element
  4557. * @return {HTMLElement} axis
  4558. */
  4559. ItemSet.prototype.getAxis = function getAxis() {
  4560. return this.dom.axis;
  4561. };
  4562. /**
  4563. * Get the element for the labelset
  4564. * @return {HTMLElement} labelSet
  4565. */
  4566. ItemSet.prototype.getLabelSet = function getLabelSet() {
  4567. return this.dom.labelSet;
  4568. };
  4569. /**
  4570. * Set items
  4571. * @param {vis.DataSet | null} items
  4572. */
  4573. ItemSet.prototype.setItems = function setItems(items) {
  4574. var me = this,
  4575. ids,
  4576. oldItemsData = this.itemsData;
  4577. // replace the dataset
  4578. if (!items) {
  4579. this.itemsData = null;
  4580. }
  4581. else if (items instanceof DataSet || items instanceof DataView) {
  4582. this.itemsData = items;
  4583. }
  4584. else {
  4585. throw new TypeError('Data must be an instance of DataSet or DataView');
  4586. }
  4587. if (oldItemsData) {
  4588. // unsubscribe from old dataset
  4589. util.forEach(this.itemListeners, function (callback, event) {
  4590. oldItemsData.unsubscribe(event, callback);
  4591. });
  4592. // remove all drawn items
  4593. ids = oldItemsData.getIds();
  4594. this._onRemove(ids);
  4595. }
  4596. if (this.itemsData) {
  4597. // subscribe to new dataset
  4598. var id = this.id;
  4599. util.forEach(this.itemListeners, function (callback, event) {
  4600. me.itemsData.on(event, callback, id);
  4601. });
  4602. // add all new items
  4603. ids = this.itemsData.getIds();
  4604. this._onAdd(ids);
  4605. // update the group holding all ungrouped items
  4606. this._updateUngrouped();
  4607. }
  4608. };
  4609. /**
  4610. * Get the current items
  4611. * @returns {vis.DataSet | null}
  4612. */
  4613. ItemSet.prototype.getItems = function getItems() {
  4614. return this.itemsData;
  4615. };
  4616. /**
  4617. * Set groups
  4618. * @param {vis.DataSet} groups
  4619. */
  4620. ItemSet.prototype.setGroups = function setGroups(groups) {
  4621. var me = this,
  4622. ids;
  4623. // unsubscribe from current dataset
  4624. if (this.groupsData) {
  4625. util.forEach(this.groupListeners, function (callback, event) {
  4626. me.groupsData.unsubscribe(event, callback);
  4627. });
  4628. // remove all drawn groups
  4629. ids = this.groupsData.getIds();
  4630. this.groupsData = null;
  4631. this._onRemoveGroups(ids); // note: this will cause a repaint
  4632. }
  4633. // replace the dataset
  4634. if (!groups) {
  4635. this.groupsData = null;
  4636. }
  4637. else if (groups instanceof DataSet || groups instanceof DataView) {
  4638. this.groupsData = groups;
  4639. }
  4640. else {
  4641. throw new TypeError('Data must be an instance of DataSet or DataView');
  4642. }
  4643. if (this.groupsData) {
  4644. // subscribe to new dataset
  4645. var id = this.id;
  4646. util.forEach(this.groupListeners, function (callback, event) {
  4647. me.groupsData.on(event, callback, id);
  4648. });
  4649. // draw all ms
  4650. ids = this.groupsData.getIds();
  4651. this._onAddGroups(ids);
  4652. }
  4653. // update the group holding all ungrouped items
  4654. this._updateUngrouped();
  4655. // update the order of all items in each group
  4656. this._order();
  4657. this.emit('change');
  4658. };
  4659. /**
  4660. * Get the current groups
  4661. * @returns {vis.DataSet | null} groups
  4662. */
  4663. ItemSet.prototype.getGroups = function getGroups() {
  4664. return this.groupsData;
  4665. };
  4666. /**
  4667. * Remove an item by its id
  4668. * @param {String | Number} id
  4669. */
  4670. ItemSet.prototype.removeItem = function removeItem (id) {
  4671. var item = this.itemsData.get(id),
  4672. dataset = this._myDataSet();
  4673. if (item) {
  4674. // confirm deletion
  4675. this.options.onRemove(item, function (item) {
  4676. if (item) {
  4677. // remove by id here, it is possible that an item has no id defined
  4678. // itself, so better not delete by the item itself
  4679. dataset.remove(id);
  4680. }
  4681. });
  4682. }
  4683. };
  4684. /**
  4685. * Handle updated items
  4686. * @param {Number[]} ids
  4687. * @protected
  4688. */
  4689. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4690. var me = this,
  4691. items = this.items,
  4692. itemOptions = this.itemOptions;
  4693. ids.forEach(function (id) {
  4694. var itemData = me.itemsData.get(id),
  4695. item = items[id],
  4696. type = itemData.type ||
  4697. (itemData.start && itemData.end && 'range') ||
  4698. me.options.type ||
  4699. 'box';
  4700. var constructor = ItemSet.types[type];
  4701. if (item) {
  4702. // update item
  4703. if (!constructor || !(item instanceof constructor)) {
  4704. // item type has changed, delete the item and recreate it
  4705. me._removeItem(item);
  4706. item = null;
  4707. }
  4708. else {
  4709. me._updateItem(item, itemData);
  4710. }
  4711. }
  4712. if (!item) {
  4713. // create item
  4714. if (constructor) {
  4715. item = new constructor(itemData, me.options, itemOptions);
  4716. item.id = id; // TODO: not so nice setting id afterwards
  4717. me._addItem(item);
  4718. }
  4719. else {
  4720. throw new TypeError('Unknown item type "' + type + '"');
  4721. }
  4722. }
  4723. });
  4724. this._order();
  4725. this.stackDirty = true; // force re-stacking of all items next repaint
  4726. this.emit('change');
  4727. };
  4728. /**
  4729. * Handle added items
  4730. * @param {Number[]} ids
  4731. * @protected
  4732. */
  4733. ItemSet.prototype._onAdd = ItemSet.prototype._onUpdate;
  4734. /**
  4735. * Handle removed items
  4736. * @param {Number[]} ids
  4737. * @protected
  4738. */
  4739. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4740. var count = 0;
  4741. var me = this;
  4742. ids.forEach(function (id) {
  4743. var item = me.items[id];
  4744. if (item) {
  4745. count++;
  4746. me._removeItem(item);
  4747. }
  4748. });
  4749. if (count) {
  4750. // update order
  4751. this._order();
  4752. this.stackDirty = true; // force re-stacking of all items next repaint
  4753. this.emit('change');
  4754. }
  4755. };
  4756. /**
  4757. * Update the order of item in all groups
  4758. * @private
  4759. */
  4760. ItemSet.prototype._order = function _order() {
  4761. // reorder the items in all groups
  4762. // TODO: optimization: only reorder groups affected by the changed items
  4763. util.forEach(this.groups, function (group) {
  4764. group.order();
  4765. });
  4766. };
  4767. /**
  4768. * Handle updated groups
  4769. * @param {Number[]} ids
  4770. * @private
  4771. */
  4772. ItemSet.prototype._onUpdateGroups = function _onUpdateGroups(ids) {
  4773. this._onAddGroups(ids);
  4774. };
  4775. /**
  4776. * Handle changed groups
  4777. * @param {Number[]} ids
  4778. * @private
  4779. */
  4780. ItemSet.prototype._onAddGroups = function _onAddGroups(ids) {
  4781. var me = this;
  4782. ids.forEach(function (id) {
  4783. var groupData = me.groupsData.get(id);
  4784. var group = me.groups[id];
  4785. if (!group) {
  4786. // check for reserved ids
  4787. if (id == UNGROUPED) {
  4788. throw new Error('Illegal group id. ' + id + ' is a reserved id.');
  4789. }
  4790. var groupOptions = Object.create(me.options);
  4791. util.extend(groupOptions, {
  4792. height: null
  4793. });
  4794. group = new Group(id, groupData, me);
  4795. me.groups[id] = group;
  4796. // add items with this groupId to the new group
  4797. for (var itemId in me.items) {
  4798. if (me.items.hasOwnProperty(itemId)) {
  4799. var item = me.items[itemId];
  4800. if (item.data.group == id) {
  4801. group.add(item);
  4802. }
  4803. }
  4804. }
  4805. group.order();
  4806. group.show();
  4807. }
  4808. else {
  4809. // update group
  4810. group.setData(groupData);
  4811. }
  4812. });
  4813. this.emit('change');
  4814. };
  4815. /**
  4816. * Handle removed groups
  4817. * @param {Number[]} ids
  4818. * @private
  4819. */
  4820. ItemSet.prototype._onRemoveGroups = function _onRemoveGroups(ids) {
  4821. var groups = this.groups;
  4822. ids.forEach(function (id) {
  4823. var group = groups[id];
  4824. if (group) {
  4825. group.hide();
  4826. delete groups[id];
  4827. }
  4828. });
  4829. this.markDirty();
  4830. this.emit('change');
  4831. };
  4832. /**
  4833. * Reorder the groups if needed
  4834. * @return {boolean} changed
  4835. * @private
  4836. */
  4837. ItemSet.prototype._orderGroups = function () {
  4838. if (this.groupsData) {
  4839. // reorder the groups
  4840. var groupIds = this.groupsData.getIds({
  4841. order: this.options.groupOrder
  4842. });
  4843. var changed = !util.equalArray(groupIds, this.groupIds);
  4844. if (changed) {
  4845. // hide all groups, removes them from the DOM
  4846. var groups = this.groups;
  4847. groupIds.forEach(function (groupId) {
  4848. groups[groupId].hide();
  4849. });
  4850. // show the groups again, attach them to the DOM in correct order
  4851. groupIds.forEach(function (groupId) {
  4852. groups[groupId].show();
  4853. });
  4854. this.groupIds = groupIds;
  4855. }
  4856. return changed;
  4857. }
  4858. else {
  4859. return false;
  4860. }
  4861. };
  4862. /**
  4863. * Add a new item
  4864. * @param {Item} item
  4865. * @private
  4866. */
  4867. ItemSet.prototype._addItem = function _addItem(item) {
  4868. this.items[item.id] = item;
  4869. // add to group
  4870. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  4871. var group = this.groups[groupId];
  4872. if (group) group.add(item);
  4873. };
  4874. /**
  4875. * Update an existing item
  4876. * @param {Item} item
  4877. * @param {Object} itemData
  4878. * @private
  4879. */
  4880. ItemSet.prototype._updateItem = function _updateItem(item, itemData) {
  4881. var oldGroupId = item.data.group;
  4882. item.data = itemData;
  4883. if (item.displayed) {
  4884. item.repaint();
  4885. }
  4886. // update group
  4887. if (oldGroupId != item.data.group) {
  4888. var oldGroup = this.groups[oldGroupId];
  4889. if (oldGroup) oldGroup.remove(item);
  4890. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  4891. var group = this.groups[groupId];
  4892. if (group) group.add(item);
  4893. }
  4894. };
  4895. /**
  4896. * Delete an item from the ItemSet: remove it from the DOM, from the map
  4897. * with items, and from the map with visible items, and from the selection
  4898. * @param {Item} item
  4899. * @private
  4900. */
  4901. ItemSet.prototype._removeItem = function _removeItem(item) {
  4902. // remove from DOM
  4903. item.hide();
  4904. // remove from items
  4905. delete this.items[item.id];
  4906. // remove from selection
  4907. var index = this.selection.indexOf(item.id);
  4908. if (index != -1) this.selection.splice(index, 1);
  4909. // remove from group
  4910. var groupId = this.groupsData ? item.data.group : UNGROUPED;
  4911. var group = this.groups[groupId];
  4912. if (group) group.remove(item);
  4913. };
  4914. /**
  4915. * Create an array containing all items being a range (having an end date)
  4916. * @param array
  4917. * @returns {Array}
  4918. * @private
  4919. */
  4920. ItemSet.prototype._constructByEndArray = function _constructByEndArray(array) {
  4921. var endArray = [];
  4922. for (var i = 0; i < array.length; i++) {
  4923. if (array[i] instanceof ItemRange) {
  4924. endArray.push(array[i]);
  4925. }
  4926. }
  4927. return endArray;
  4928. };
  4929. /**
  4930. * Get the width of the group labels
  4931. * @return {Number} width
  4932. */
  4933. ItemSet.prototype.getLabelsWidth = function getLabelsWidth() {
  4934. var width = 0;
  4935. util.forEach(this.groups, function (group) {
  4936. width = Math.max(width, group.getLabelWidth());
  4937. });
  4938. return width;
  4939. };
  4940. /**
  4941. * Get the height of the itemsets background
  4942. * @return {Number} height
  4943. */
  4944. ItemSet.prototype.getBackgroundHeight = function getBackgroundHeight() {
  4945. return this.height;
  4946. };
  4947. /**
  4948. * Start dragging the selected events
  4949. * @param {Event} event
  4950. * @private
  4951. */
  4952. ItemSet.prototype._onDragStart = function (event) {
  4953. if (!this.options.editable.updateTime && !this.options.editable.updateGroup) {
  4954. return;
  4955. }
  4956. var item = ItemSet.itemFromTarget(event),
  4957. me = this,
  4958. props;
  4959. if (item && item.selected) {
  4960. var dragLeftItem = event.target.dragLeftItem;
  4961. var dragRightItem = event.target.dragRightItem;
  4962. if (dragLeftItem) {
  4963. props = {
  4964. item: dragLeftItem
  4965. };
  4966. if (me.options.editable.updateTime) {
  4967. props.start = item.data.start.valueOf();
  4968. }
  4969. if (me.options.editable.updateGroup) {
  4970. if ('group' in item.data) props.group = item.data.group;
  4971. }
  4972. this.touchParams.itemProps = [props];
  4973. }
  4974. else if (dragRightItem) {
  4975. props = {
  4976. item: dragRightItem
  4977. };
  4978. if (me.options.editable.updateTime) {
  4979. props.end = item.data.end.valueOf();
  4980. }
  4981. if (me.options.editable.updateGroup) {
  4982. if ('group' in item.data) props.group = item.data.group;
  4983. }
  4984. this.touchParams.itemProps = [props];
  4985. }
  4986. else {
  4987. this.touchParams.itemProps = this.getSelection().map(function (id) {
  4988. var item = me.items[id];
  4989. var props = {
  4990. item: item
  4991. };
  4992. if (me.options.editable.updateTime) {
  4993. if ('start' in item.data) props.start = item.data.start.valueOf();
  4994. if ('end' in item.data) props.end = item.data.end.valueOf();
  4995. }
  4996. if (me.options.editable.updateGroup) {
  4997. if ('group' in item.data) props.group = item.data.group;
  4998. }
  4999. return props;
  5000. });
  5001. }
  5002. event.stopPropagation();
  5003. }
  5004. };
  5005. /**
  5006. * Drag selected items
  5007. * @param {Event} event
  5008. * @private
  5009. */
  5010. ItemSet.prototype._onDrag = function (event) {
  5011. if (this.touchParams.itemProps) {
  5012. var snap = this.options.snap || null,
  5013. deltaX = event.gesture.deltaX,
  5014. scale = (this.width / (this.range.end - this.range.start)),
  5015. offset = deltaX / scale;
  5016. // move
  5017. this.touchParams.itemProps.forEach(function (props) {
  5018. if ('start' in props) {
  5019. var start = new Date(props.start + offset);
  5020. props.item.data.start = snap ? snap(start) : start;
  5021. }
  5022. if ('end' in props) {
  5023. var end = new Date(props.end + offset);
  5024. props.item.data.end = snap ? snap(end) : end;
  5025. }
  5026. if ('group' in props) {
  5027. // drag from one group to another
  5028. var group = ItemSet.groupFromTarget(event);
  5029. if (group && group.groupId != props.item.data.group) {
  5030. var oldGroup = props.item.parent;
  5031. oldGroup.remove(props.item);
  5032. oldGroup.order();
  5033. group.add(props.item);
  5034. group.order();
  5035. props.item.data.group = group.groupId;
  5036. }
  5037. }
  5038. });
  5039. // TODO: implement onMoving handler
  5040. this.stackDirty = true; // force re-stacking of all items next repaint
  5041. this.emit('change');
  5042. event.stopPropagation();
  5043. }
  5044. };
  5045. /**
  5046. * End of dragging selected items
  5047. * @param {Event} event
  5048. * @private
  5049. */
  5050. ItemSet.prototype._onDragEnd = function (event) {
  5051. if (this.touchParams.itemProps) {
  5052. // prepare a change set for the changed items
  5053. var changes = [],
  5054. me = this,
  5055. dataset = this._myDataSet();
  5056. this.touchParams.itemProps.forEach(function (props) {
  5057. var id = props.item.id,
  5058. itemData = me.itemsData.get(id);
  5059. var changed = false;
  5060. if ('start' in props.item.data) {
  5061. changed = (props.start != props.item.data.start.valueOf());
  5062. itemData.start = util.convert(props.item.data.start, dataset.convert['start']);
  5063. }
  5064. if ('end' in props.item.data) {
  5065. changed = changed || (props.end != props.item.data.end.valueOf());
  5066. itemData.end = util.convert(props.item.data.end, dataset.convert['end']);
  5067. }
  5068. if ('group' in props.item.data) {
  5069. changed = changed || (props.group != props.item.data.group);
  5070. itemData.group = props.item.data.group;
  5071. }
  5072. // only apply changes when start or end is actually changed
  5073. if (changed) {
  5074. me.options.onMove(itemData, function (itemData) {
  5075. if (itemData) {
  5076. // apply changes
  5077. itemData[dataset.fieldId] = id; // ensure the item contains its id (can be undefined)
  5078. changes.push(itemData);
  5079. }
  5080. else {
  5081. // restore original values
  5082. if ('start' in props) props.item.data.start = props.start;
  5083. if ('end' in props) props.item.data.end = props.end;
  5084. me.stackDirty = true; // force re-stacking of all items next repaint
  5085. me.emit('change');
  5086. }
  5087. });
  5088. }
  5089. });
  5090. this.touchParams.itemProps = null;
  5091. // apply the changes to the data (if there are changes)
  5092. if (changes.length) {
  5093. dataset.update(changes);
  5094. }
  5095. event.stopPropagation();
  5096. }
  5097. };
  5098. /**
  5099. * Find an item from an event target:
  5100. * searches for the attribute 'timeline-item' in the event target's element tree
  5101. * @param {Event} event
  5102. * @return {Item | null} item
  5103. */
  5104. ItemSet.itemFromTarget = function itemFromTarget (event) {
  5105. var target = event.target;
  5106. while (target) {
  5107. if (target.hasOwnProperty('timeline-item')) {
  5108. return target['timeline-item'];
  5109. }
  5110. target = target.parentNode;
  5111. }
  5112. return null;
  5113. };
  5114. /**
  5115. * Find the Group from an event target:
  5116. * searches for the attribute 'timeline-group' in the event target's element tree
  5117. * @param {Event} event
  5118. * @return {Group | null} group
  5119. */
  5120. ItemSet.groupFromTarget = function groupFromTarget (event) {
  5121. var target = event.target;
  5122. while (target) {
  5123. if (target.hasOwnProperty('timeline-group')) {
  5124. return target['timeline-group'];
  5125. }
  5126. target = target.parentNode;
  5127. }
  5128. return null;
  5129. };
  5130. /**
  5131. * Find the ItemSet from an event target:
  5132. * searches for the attribute 'timeline-itemset' in the event target's element tree
  5133. * @param {Event} event
  5134. * @return {ItemSet | null} item
  5135. */
  5136. ItemSet.itemSetFromTarget = function itemSetFromTarget (event) {
  5137. var target = event.target;
  5138. while (target) {
  5139. if (target.hasOwnProperty('timeline-itemset')) {
  5140. return target['timeline-itemset'];
  5141. }
  5142. target = target.parentNode;
  5143. }
  5144. return null;
  5145. };
  5146. /**
  5147. * Find the DataSet to which this ItemSet is connected
  5148. * @returns {null | DataSet} dataset
  5149. * @private
  5150. */
  5151. ItemSet.prototype._myDataSet = function _myDataSet() {
  5152. // find the root DataSet
  5153. var dataset = this.itemsData;
  5154. while (dataset instanceof DataView) {
  5155. dataset = dataset.data;
  5156. }
  5157. return dataset;
  5158. };
  5159. /**
  5160. * @constructor Item
  5161. * @param {Object} data Object containing (optional) parameters type,
  5162. * start, end, content, group, className.
  5163. * @param {Object} [options] Options to set initial property values
  5164. * @param {Object} [defaultOptions] default options
  5165. * // TODO: describe available options
  5166. */
  5167. function Item (data, options, defaultOptions) {
  5168. this.id = null;
  5169. this.parent = null;
  5170. this.data = data;
  5171. this.dom = null;
  5172. this.options = options || {};
  5173. this.defaultOptions = defaultOptions || {};
  5174. this.selected = false;
  5175. this.displayed = false;
  5176. this.dirty = true;
  5177. this.top = null;
  5178. this.left = null;
  5179. this.width = null;
  5180. this.height = null;
  5181. }
  5182. /**
  5183. * Select current item
  5184. */
  5185. Item.prototype.select = function select() {
  5186. this.selected = true;
  5187. if (this.displayed) this.repaint();
  5188. };
  5189. /**
  5190. * Unselect current item
  5191. */
  5192. Item.prototype.unselect = function unselect() {
  5193. this.selected = false;
  5194. if (this.displayed) this.repaint();
  5195. };
  5196. /**
  5197. * Set a parent for the item
  5198. * @param {ItemSet | Group} parent
  5199. */
  5200. Item.prototype.setParent = function setParent(parent) {
  5201. if (this.displayed) {
  5202. this.hide();
  5203. this.parent = parent;
  5204. if (this.parent) {
  5205. this.show();
  5206. }
  5207. }
  5208. else {
  5209. this.parent = parent;
  5210. }
  5211. };
  5212. /**
  5213. * Check whether this item is visible inside given range
  5214. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5215. * @returns {boolean} True if visible
  5216. */
  5217. Item.prototype.isVisible = function isVisible (range) {
  5218. // Should be implemented by Item implementations
  5219. return false;
  5220. };
  5221. /**
  5222. * Show the Item in the DOM (when not already visible)
  5223. * @return {Boolean} changed
  5224. */
  5225. Item.prototype.show = function show() {
  5226. return false;
  5227. };
  5228. /**
  5229. * Hide the Item from the DOM (when visible)
  5230. * @return {Boolean} changed
  5231. */
  5232. Item.prototype.hide = function hide() {
  5233. return false;
  5234. };
  5235. /**
  5236. * Repaint the item
  5237. */
  5238. Item.prototype.repaint = function repaint() {
  5239. // should be implemented by the item
  5240. };
  5241. /**
  5242. * Reposition the Item horizontally
  5243. */
  5244. Item.prototype.repositionX = function repositionX() {
  5245. // should be implemented by the item
  5246. };
  5247. /**
  5248. * Reposition the Item vertically
  5249. */
  5250. Item.prototype.repositionY = function repositionY() {
  5251. // should be implemented by the item
  5252. };
  5253. /**
  5254. * Repaint a delete button on the top right of the item when the item is selected
  5255. * @param {HTMLElement} anchor
  5256. * @protected
  5257. */
  5258. Item.prototype._repaintDeleteButton = function (anchor) {
  5259. if (this.selected && this.options.editable.remove && !this.dom.deleteButton) {
  5260. // create and show button
  5261. var me = this;
  5262. var deleteButton = document.createElement('div');
  5263. deleteButton.className = 'delete';
  5264. deleteButton.title = 'Delete this item';
  5265. Hammer(deleteButton, {
  5266. preventDefault: true
  5267. }).on('tap', function (event) {
  5268. me.parent.removeFromDataSet(me);
  5269. event.stopPropagation();
  5270. });
  5271. anchor.appendChild(deleteButton);
  5272. this.dom.deleteButton = deleteButton;
  5273. }
  5274. else if (!this.selected && this.dom.deleteButton) {
  5275. // remove button
  5276. if (this.dom.deleteButton.parentNode) {
  5277. this.dom.deleteButton.parentNode.removeChild(this.dom.deleteButton);
  5278. }
  5279. this.dom.deleteButton = null;
  5280. }
  5281. };
  5282. /**
  5283. * @constructor ItemBox
  5284. * @extends Item
  5285. * @param {Object} data Object containing parameters start
  5286. * content, className.
  5287. * @param {Object} [options] Options to set initial property values
  5288. * @param {Object} [defaultOptions] default options
  5289. * // TODO: describe available options
  5290. */
  5291. function ItemBox (data, options, defaultOptions) {
  5292. this.props = {
  5293. dot: {
  5294. width: 0,
  5295. height: 0
  5296. },
  5297. line: {
  5298. width: 0,
  5299. height: 0
  5300. }
  5301. };
  5302. // validate data
  5303. if (data) {
  5304. if (data.start == undefined) {
  5305. throw new Error('Property "start" missing in item ' + data);
  5306. }
  5307. }
  5308. Item.call(this, data, options, defaultOptions);
  5309. }
  5310. ItemBox.prototype = new Item (null);
  5311. /**
  5312. * Check whether this item is visible inside given range
  5313. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5314. * @returns {boolean} True if visible
  5315. */
  5316. ItemBox.prototype.isVisible = function isVisible (range) {
  5317. // determine visibility
  5318. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  5319. var interval = (range.end - range.start) / 4;
  5320. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  5321. };
  5322. /**
  5323. * Repaint the item
  5324. */
  5325. ItemBox.prototype.repaint = function repaint() {
  5326. var dom = this.dom;
  5327. if (!dom) {
  5328. // create DOM
  5329. this.dom = {};
  5330. dom = this.dom;
  5331. // create main box
  5332. dom.box = document.createElement('DIV');
  5333. // contents box (inside the background box). used for making margins
  5334. dom.content = document.createElement('DIV');
  5335. dom.content.className = 'content';
  5336. dom.box.appendChild(dom.content);
  5337. // line to axis
  5338. dom.line = document.createElement('DIV');
  5339. dom.line.className = 'line';
  5340. // dot on axis
  5341. dom.dot = document.createElement('DIV');
  5342. dom.dot.className = 'dot';
  5343. // attach this item as attribute
  5344. dom.box['timeline-item'] = this;
  5345. }
  5346. // append DOM to parent DOM
  5347. if (!this.parent) {
  5348. throw new Error('Cannot repaint item: no parent attached');
  5349. }
  5350. if (!dom.box.parentNode) {
  5351. var foreground = this.parent.getForeground();
  5352. if (!foreground) throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5353. foreground.appendChild(dom.box);
  5354. }
  5355. if (!dom.line.parentNode) {
  5356. var background = this.parent.getBackground();
  5357. if (!background) throw new Error('Cannot repaint time axis: parent has no background container element');
  5358. background.appendChild(dom.line);
  5359. }
  5360. if (!dom.dot.parentNode) {
  5361. var axis = this.parent.getAxis();
  5362. if (!background) throw new Error('Cannot repaint time axis: parent has no axis container element');
  5363. axis.appendChild(dom.dot);
  5364. }
  5365. this.displayed = true;
  5366. // update contents
  5367. if (this.data.content != this.content) {
  5368. this.content = this.data.content;
  5369. if (this.content instanceof Element) {
  5370. dom.content.innerHTML = '';
  5371. dom.content.appendChild(this.content);
  5372. }
  5373. else if (this.data.content != undefined) {
  5374. dom.content.innerHTML = this.content;
  5375. }
  5376. else {
  5377. throw new Error('Property "content" missing in item ' + this.data.id);
  5378. }
  5379. this.dirty = true;
  5380. }
  5381. // update class
  5382. var className = (this.data.className? ' ' + this.data.className : '') +
  5383. (this.selected ? ' selected' : '');
  5384. if (this.className != className) {
  5385. this.className = className;
  5386. dom.box.className = 'item box' + className;
  5387. dom.line.className = 'item line' + className;
  5388. dom.dot.className = 'item dot' + className;
  5389. this.dirty = true;
  5390. }
  5391. // recalculate size
  5392. if (this.dirty) {
  5393. this.props.dot.height = dom.dot.offsetHeight;
  5394. this.props.dot.width = dom.dot.offsetWidth;
  5395. this.props.line.width = dom.line.offsetWidth;
  5396. this.width = dom.box.offsetWidth;
  5397. this.height = dom.box.offsetHeight;
  5398. this.dirty = false;
  5399. }
  5400. this._repaintDeleteButton(dom.box);
  5401. };
  5402. /**
  5403. * Show the item in the DOM (when not already displayed). The items DOM will
  5404. * be created when needed.
  5405. */
  5406. ItemBox.prototype.show = function show() {
  5407. if (!this.displayed) {
  5408. this.repaint();
  5409. }
  5410. };
  5411. /**
  5412. * Hide the item from the DOM (when visible)
  5413. */
  5414. ItemBox.prototype.hide = function hide() {
  5415. if (this.displayed) {
  5416. var dom = this.dom;
  5417. if (dom.box.parentNode) dom.box.parentNode.removeChild(dom.box);
  5418. if (dom.line.parentNode) dom.line.parentNode.removeChild(dom.line);
  5419. if (dom.dot.parentNode) dom.dot.parentNode.removeChild(dom.dot);
  5420. this.top = null;
  5421. this.left = null;
  5422. this.displayed = false;
  5423. }
  5424. };
  5425. /**
  5426. * Reposition the item horizontally
  5427. * @Override
  5428. */
  5429. ItemBox.prototype.repositionX = function repositionX() {
  5430. var start = this.defaultOptions.toScreen(this.data.start),
  5431. align = this.options.align || this.defaultOptions.align,
  5432. left,
  5433. box = this.dom.box,
  5434. line = this.dom.line,
  5435. dot = this.dom.dot;
  5436. // calculate left position of the box
  5437. if (align == 'right') {
  5438. this.left = start - this.width;
  5439. }
  5440. else if (align == 'left') {
  5441. this.left = start;
  5442. }
  5443. else {
  5444. // default or 'center'
  5445. this.left = start - this.width / 2;
  5446. }
  5447. // reposition box
  5448. box.style.left = this.left + 'px';
  5449. // reposition line
  5450. line.style.left = (start - this.props.line.width / 2) + 'px';
  5451. // reposition dot
  5452. dot.style.left = (start - this.props.dot.width / 2) + 'px';
  5453. };
  5454. /**
  5455. * Reposition the item vertically
  5456. * @Override
  5457. */
  5458. ItemBox.prototype.repositionY = function repositionY () {
  5459. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5460. box = this.dom.box,
  5461. line = this.dom.line,
  5462. dot = this.dom.dot;
  5463. if (orientation == 'top') {
  5464. box.style.top = (this.top || 0) + 'px';
  5465. box.style.bottom = '';
  5466. line.style.top = '0';
  5467. line.style.bottom = '';
  5468. line.style.height = (this.parent.top + this.top + 1) + 'px';
  5469. }
  5470. else { // orientation 'bottom'
  5471. box.style.top = '';
  5472. box.style.bottom = (this.top || 0) + 'px';
  5473. line.style.top = (this.parent.top + this.parent.height - this.top - 1) + 'px';
  5474. line.style.bottom = '0';
  5475. line.style.height = '';
  5476. }
  5477. dot.style.top = (-this.props.dot.height / 2) + 'px';
  5478. };
  5479. /**
  5480. * @constructor ItemPoint
  5481. * @extends Item
  5482. * @param {Object} data Object containing parameters start
  5483. * content, className.
  5484. * @param {Object} [options] Options to set initial property values
  5485. * @param {Object} [defaultOptions] default options
  5486. * // TODO: describe available options
  5487. */
  5488. function ItemPoint (data, options, defaultOptions) {
  5489. this.props = {
  5490. dot: {
  5491. top: 0,
  5492. width: 0,
  5493. height: 0
  5494. },
  5495. content: {
  5496. height: 0,
  5497. marginLeft: 0
  5498. }
  5499. };
  5500. // validate data
  5501. if (data) {
  5502. if (data.start == undefined) {
  5503. throw new Error('Property "start" missing in item ' + data);
  5504. }
  5505. }
  5506. Item.call(this, data, options, defaultOptions);
  5507. }
  5508. ItemPoint.prototype = new Item (null);
  5509. /**
  5510. * Check whether this item is visible inside given range
  5511. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5512. * @returns {boolean} True if visible
  5513. */
  5514. ItemPoint.prototype.isVisible = function isVisible (range) {
  5515. // determine visibility
  5516. // TODO: account for the real width of the item. Right now we just add 1/4 to the window
  5517. var interval = (range.end - range.start) / 4;
  5518. return (this.data.start > range.start - interval) && (this.data.start < range.end + interval);
  5519. };
  5520. /**
  5521. * Repaint the item
  5522. */
  5523. ItemPoint.prototype.repaint = function repaint() {
  5524. var dom = this.dom;
  5525. if (!dom) {
  5526. // create DOM
  5527. this.dom = {};
  5528. dom = this.dom;
  5529. // background box
  5530. dom.point = document.createElement('div');
  5531. // className is updated in repaint()
  5532. // contents box, right from the dot
  5533. dom.content = document.createElement('div');
  5534. dom.content.className = 'content';
  5535. dom.point.appendChild(dom.content);
  5536. // dot at start
  5537. dom.dot = document.createElement('div');
  5538. dom.point.appendChild(dom.dot);
  5539. // attach this item as attribute
  5540. dom.point['timeline-item'] = this;
  5541. }
  5542. // append DOM to parent DOM
  5543. if (!this.parent) {
  5544. throw new Error('Cannot repaint item: no parent attached');
  5545. }
  5546. if (!dom.point.parentNode) {
  5547. var foreground = this.parent.getForeground();
  5548. if (!foreground) {
  5549. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5550. }
  5551. foreground.appendChild(dom.point);
  5552. }
  5553. this.displayed = true;
  5554. // update contents
  5555. if (this.data.content != this.content) {
  5556. this.content = this.data.content;
  5557. if (this.content instanceof Element) {
  5558. dom.content.innerHTML = '';
  5559. dom.content.appendChild(this.content);
  5560. }
  5561. else if (this.data.content != undefined) {
  5562. dom.content.innerHTML = this.content;
  5563. }
  5564. else {
  5565. throw new Error('Property "content" missing in item ' + this.data.id);
  5566. }
  5567. this.dirty = true;
  5568. }
  5569. // update class
  5570. var className = (this.data.className? ' ' + this.data.className : '') +
  5571. (this.selected ? ' selected' : '');
  5572. if (this.className != className) {
  5573. this.className = className;
  5574. dom.point.className = 'item point' + className;
  5575. dom.dot.className = 'item dot' + className;
  5576. this.dirty = true;
  5577. }
  5578. // recalculate size
  5579. if (this.dirty) {
  5580. this.width = dom.point.offsetWidth;
  5581. this.height = dom.point.offsetHeight;
  5582. this.props.dot.width = dom.dot.offsetWidth;
  5583. this.props.dot.height = dom.dot.offsetHeight;
  5584. this.props.content.height = dom.content.offsetHeight;
  5585. // resize contents
  5586. dom.content.style.marginLeft = 2 * this.props.dot.width + 'px';
  5587. //dom.content.style.marginRight = ... + 'px'; // TODO: margin right
  5588. dom.dot.style.top = ((this.height - this.props.dot.height) / 2) + 'px';
  5589. dom.dot.style.left = (this.props.dot.width / 2) + 'px';
  5590. this.dirty = false;
  5591. }
  5592. this._repaintDeleteButton(dom.point);
  5593. };
  5594. /**
  5595. * Show the item in the DOM (when not already visible). The items DOM will
  5596. * be created when needed.
  5597. */
  5598. ItemPoint.prototype.show = function show() {
  5599. if (!this.displayed) {
  5600. this.repaint();
  5601. }
  5602. };
  5603. /**
  5604. * Hide the item from the DOM (when visible)
  5605. */
  5606. ItemPoint.prototype.hide = function hide() {
  5607. if (this.displayed) {
  5608. if (this.dom.point.parentNode) {
  5609. this.dom.point.parentNode.removeChild(this.dom.point);
  5610. }
  5611. this.top = null;
  5612. this.left = null;
  5613. this.displayed = false;
  5614. }
  5615. };
  5616. /**
  5617. * Reposition the item horizontally
  5618. * @Override
  5619. */
  5620. ItemPoint.prototype.repositionX = function repositionX() {
  5621. var start = this.defaultOptions.toScreen(this.data.start);
  5622. this.left = start - this.props.dot.width;
  5623. // reposition point
  5624. this.dom.point.style.left = this.left + 'px';
  5625. };
  5626. /**
  5627. * Reposition the item vertically
  5628. * @Override
  5629. */
  5630. ItemPoint.prototype.repositionY = function repositionY () {
  5631. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5632. point = this.dom.point;
  5633. if (orientation == 'top') {
  5634. point.style.top = this.top + 'px';
  5635. point.style.bottom = '';
  5636. }
  5637. else {
  5638. point.style.top = '';
  5639. point.style.bottom = this.top + 'px';
  5640. }
  5641. };
  5642. /**
  5643. * @constructor ItemRange
  5644. * @extends Item
  5645. * @param {Object} data Object containing parameters start, end
  5646. * content, className.
  5647. * @param {Object} [options] Options to set initial property values
  5648. * @param {Object} [defaultOptions] default options
  5649. * // TODO: describe available options
  5650. */
  5651. function ItemRange (data, options, defaultOptions) {
  5652. this.props = {
  5653. content: {
  5654. width: 0
  5655. }
  5656. };
  5657. // validate data
  5658. if (data) {
  5659. if (data.start == undefined) {
  5660. throw new Error('Property "start" missing in item ' + data.id);
  5661. }
  5662. if (data.end == undefined) {
  5663. throw new Error('Property "end" missing in item ' + data.id);
  5664. }
  5665. }
  5666. Item.call(this, data, options, defaultOptions);
  5667. }
  5668. ItemRange.prototype = new Item (null);
  5669. ItemRange.prototype.baseClassName = 'item range';
  5670. /**
  5671. * Check whether this item is visible inside given range
  5672. * @returns {{start: Number, end: Number}} range with a timestamp for start and end
  5673. * @returns {boolean} True if visible
  5674. */
  5675. ItemRange.prototype.isVisible = function isVisible (range) {
  5676. // determine visibility
  5677. return (this.data.start < range.end) && (this.data.end > range.start);
  5678. };
  5679. /**
  5680. * Repaint the item
  5681. */
  5682. ItemRange.prototype.repaint = function repaint() {
  5683. var dom = this.dom;
  5684. if (!dom) {
  5685. // create DOM
  5686. this.dom = {};
  5687. dom = this.dom;
  5688. // background box
  5689. dom.box = document.createElement('div');
  5690. // className is updated in repaint()
  5691. // contents box
  5692. dom.content = document.createElement('div');
  5693. dom.content.className = 'content';
  5694. dom.box.appendChild(dom.content);
  5695. // attach this item as attribute
  5696. dom.box['timeline-item'] = this;
  5697. }
  5698. // append DOM to parent DOM
  5699. if (!this.parent) {
  5700. throw new Error('Cannot repaint item: no parent attached');
  5701. }
  5702. if (!dom.box.parentNode) {
  5703. var foreground = this.parent.getForeground();
  5704. if (!foreground) {
  5705. throw new Error('Cannot repaint time axis: parent has no foreground container element');
  5706. }
  5707. foreground.appendChild(dom.box);
  5708. }
  5709. this.displayed = true;
  5710. // update contents
  5711. if (this.data.content != this.content) {
  5712. this.content = this.data.content;
  5713. if (this.content instanceof Element) {
  5714. dom.content.innerHTML = '';
  5715. dom.content.appendChild(this.content);
  5716. }
  5717. else if (this.data.content != undefined) {
  5718. dom.content.innerHTML = this.content;
  5719. }
  5720. else {
  5721. throw new Error('Property "content" missing in item ' + this.data.id);
  5722. }
  5723. this.dirty = true;
  5724. }
  5725. // update class
  5726. var className = (this.data.className ? (' ' + this.data.className) : '') +
  5727. (this.selected ? ' selected' : '');
  5728. if (this.className != className) {
  5729. this.className = className;
  5730. dom.box.className = this.baseClassName + className;
  5731. this.dirty = true;
  5732. }
  5733. // recalculate size
  5734. if (this.dirty) {
  5735. this.props.content.width = this.dom.content.offsetWidth;
  5736. this.height = this.dom.box.offsetHeight;
  5737. this.dirty = false;
  5738. }
  5739. this._repaintDeleteButton(dom.box);
  5740. this._repaintDragLeft();
  5741. this._repaintDragRight();
  5742. };
  5743. /**
  5744. * Show the item in the DOM (when not already visible). The items DOM will
  5745. * be created when needed.
  5746. */
  5747. ItemRange.prototype.show = function show() {
  5748. if (!this.displayed) {
  5749. this.repaint();
  5750. }
  5751. };
  5752. /**
  5753. * Hide the item from the DOM (when visible)
  5754. * @return {Boolean} changed
  5755. */
  5756. ItemRange.prototype.hide = function hide() {
  5757. if (this.displayed) {
  5758. var box = this.dom.box;
  5759. if (box.parentNode) {
  5760. box.parentNode.removeChild(box);
  5761. }
  5762. this.top = null;
  5763. this.left = null;
  5764. this.displayed = false;
  5765. }
  5766. };
  5767. /**
  5768. * Reposition the item horizontally
  5769. * @Override
  5770. */
  5771. ItemRange.prototype.repositionX = function repositionX() {
  5772. var props = this.props,
  5773. parentWidth = this.parent.width,
  5774. start = this.defaultOptions.toScreen(this.data.start),
  5775. end = this.defaultOptions.toScreen(this.data.end),
  5776. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5777. contentLeft;
  5778. // limit the width of the this, as browsers cannot draw very wide divs
  5779. if (start < -parentWidth) {
  5780. start = -parentWidth;
  5781. }
  5782. if (end > 2 * parentWidth) {
  5783. end = 2 * parentWidth;
  5784. }
  5785. // when range exceeds left of the window, position the contents at the left of the visible area
  5786. if (start < 0) {
  5787. contentLeft = Math.min(-start,
  5788. (end - start - props.content.width - 2 * padding));
  5789. // TODO: remove the need for options.padding. it's terrible.
  5790. }
  5791. else {
  5792. contentLeft = 0;
  5793. }
  5794. this.left = start;
  5795. this.width = Math.max(end - start, 1);
  5796. this.dom.box.style.left = this.left + 'px';
  5797. this.dom.box.style.width = this.width + 'px';
  5798. this.dom.content.style.left = contentLeft + 'px';
  5799. };
  5800. /**
  5801. * Reposition the item vertically
  5802. * @Override
  5803. */
  5804. ItemRange.prototype.repositionY = function repositionY() {
  5805. var orientation = this.options.orientation || this.defaultOptions.orientation,
  5806. box = this.dom.box;
  5807. if (orientation == 'top') {
  5808. box.style.top = this.top + 'px';
  5809. box.style.bottom = '';
  5810. }
  5811. else {
  5812. box.style.top = '';
  5813. box.style.bottom = this.top + 'px';
  5814. }
  5815. };
  5816. /**
  5817. * Repaint a drag area on the left side of the range when the range is selected
  5818. * @protected
  5819. */
  5820. ItemRange.prototype._repaintDragLeft = function () {
  5821. if (this.selected && this.options.editable.updateTime && !this.dom.dragLeft) {
  5822. // create and show drag area
  5823. var dragLeft = document.createElement('div');
  5824. dragLeft.className = 'drag-left';
  5825. dragLeft.dragLeftItem = this;
  5826. // TODO: this should be redundant?
  5827. Hammer(dragLeft, {
  5828. preventDefault: true
  5829. }).on('drag', function () {
  5830. //console.log('drag left')
  5831. });
  5832. this.dom.box.appendChild(dragLeft);
  5833. this.dom.dragLeft = dragLeft;
  5834. }
  5835. else if (!this.selected && this.dom.dragLeft) {
  5836. // delete drag area
  5837. if (this.dom.dragLeft.parentNode) {
  5838. this.dom.dragLeft.parentNode.removeChild(this.dom.dragLeft);
  5839. }
  5840. this.dom.dragLeft = null;
  5841. }
  5842. };
  5843. /**
  5844. * Repaint a drag area on the right side of the range when the range is selected
  5845. * @protected
  5846. */
  5847. ItemRange.prototype._repaintDragRight = function () {
  5848. if (this.selected && this.options.editable.updateTime && !this.dom.dragRight) {
  5849. // create and show drag area
  5850. var dragRight = document.createElement('div');
  5851. dragRight.className = 'drag-right';
  5852. dragRight.dragRightItem = this;
  5853. // TODO: this should be redundant?
  5854. Hammer(dragRight, {
  5855. preventDefault: true
  5856. }).on('drag', function () {
  5857. //console.log('drag right')
  5858. });
  5859. this.dom.box.appendChild(dragRight);
  5860. this.dom.dragRight = dragRight;
  5861. }
  5862. else if (!this.selected && this.dom.dragRight) {
  5863. // delete drag area
  5864. if (this.dom.dragRight.parentNode) {
  5865. this.dom.dragRight.parentNode.removeChild(this.dom.dragRight);
  5866. }
  5867. this.dom.dragRight = null;
  5868. }
  5869. };
  5870. /**
  5871. * @constructor ItemRangeOverflow
  5872. * @extends ItemRange
  5873. * @param {Object} data Object containing parameters start, end
  5874. * content, className.
  5875. * @param {Object} [options] Options to set initial property values
  5876. * @param {Object} [defaultOptions] default options
  5877. * // TODO: describe available options
  5878. */
  5879. function ItemRangeOverflow (data, options, defaultOptions) {
  5880. this.props = {
  5881. content: {
  5882. left: 0,
  5883. width: 0
  5884. }
  5885. };
  5886. ItemRange.call(this, data, options, defaultOptions);
  5887. }
  5888. ItemRangeOverflow.prototype = new ItemRange (null);
  5889. ItemRangeOverflow.prototype.baseClassName = 'item rangeoverflow';
  5890. /**
  5891. * Reposition the item horizontally
  5892. * @Override
  5893. */
  5894. ItemRangeOverflow.prototype.repositionX = function repositionX() {
  5895. var parentWidth = this.parent.width,
  5896. start = this.defaultOptions.toScreen(this.data.start),
  5897. end = this.defaultOptions.toScreen(this.data.end),
  5898. padding = 'padding' in this.options ? this.options.padding : this.defaultOptions.padding,
  5899. contentLeft;
  5900. // limit the width of the this, as browsers cannot draw very wide divs
  5901. if (start < -parentWidth) {
  5902. start = -parentWidth;
  5903. }
  5904. if (end > 2 * parentWidth) {
  5905. end = 2 * parentWidth;
  5906. }
  5907. // when range exceeds left of the window, position the contents at the left of the visible area
  5908. contentLeft = Math.max(-start, 0);
  5909. this.left = start;
  5910. var boxWidth = Math.max(end - start, 1);
  5911. this.width = boxWidth + this.props.content.width;
  5912. // Note: The calculation of width is an optimistic calculation, giving
  5913. // a width which will not change when moving the Timeline
  5914. // So no restacking needed, which is nicer for the eye
  5915. this.dom.box.style.left = this.left + 'px';
  5916. this.dom.box.style.width = boxWidth + 'px';
  5917. this.dom.content.style.left = contentLeft + 'px';
  5918. };
  5919. /**
  5920. * @constructor Group
  5921. * @param {Number | String} groupId
  5922. * @param {Object} data
  5923. * @param {ItemSet} itemSet
  5924. */
  5925. function Group (groupId, data, itemSet) {
  5926. this.groupId = groupId;
  5927. this.itemSet = itemSet;
  5928. this.dom = {};
  5929. this.props = {
  5930. label: {
  5931. width: 0,
  5932. height: 0
  5933. }
  5934. };
  5935. this.items = {}; // items filtered by groupId of this group
  5936. this.visibleItems = []; // items currently visible in window
  5937. this.orderedItems = { // items sorted by start and by end
  5938. byStart: [],
  5939. byEnd: []
  5940. };
  5941. this._create();
  5942. this.setData(data);
  5943. }
  5944. /**
  5945. * Create DOM elements for the group
  5946. * @private
  5947. */
  5948. Group.prototype._create = function() {
  5949. var label = document.createElement('div');
  5950. label.className = 'vlabel';
  5951. this.dom.label = label;
  5952. var inner = document.createElement('div');
  5953. inner.className = 'inner';
  5954. label.appendChild(inner);
  5955. this.dom.inner = inner;
  5956. var foreground = document.createElement('div');
  5957. foreground.className = 'group';
  5958. foreground['timeline-group'] = this;
  5959. this.dom.foreground = foreground;
  5960. this.dom.background = document.createElement('div');
  5961. this.dom.axis = document.createElement('div');
  5962. // create a hidden marker to detect when the Timelines container is attached
  5963. // to the DOM, or the style of a parent of the Timeline is changed from
  5964. // display:none is changed to visible.
  5965. this.dom.marker = document.createElement('div');
  5966. this.dom.marker.style.visibility = 'hidden';
  5967. this.dom.marker.innerHTML = '?';
  5968. this.dom.background.appendChild(this.dom.marker);
  5969. };
  5970. /**
  5971. * Set the group data for this group
  5972. * @param {Object} data Group data, can contain properties content and className
  5973. */
  5974. Group.prototype.setData = function setData(data) {
  5975. // update contents
  5976. var content = data && data.content;
  5977. if (content instanceof Element) {
  5978. this.dom.inner.appendChild(content);
  5979. }
  5980. else if (content != undefined) {
  5981. this.dom.inner.innerHTML = content;
  5982. }
  5983. else {
  5984. this.dom.inner.innerHTML = this.groupId;
  5985. }
  5986. // update className
  5987. var className = data && data.className;
  5988. if (className) {
  5989. util.addClassName(this.dom.label, className);
  5990. }
  5991. };
  5992. /**
  5993. * Get the foreground container element
  5994. * @return {HTMLElement} foreground
  5995. */
  5996. Group.prototype.getForeground = function getForeground() {
  5997. return this.dom.foreground;
  5998. };
  5999. /**
  6000. * Get the background container element
  6001. * @return {HTMLElement} background
  6002. */
  6003. Group.prototype.getBackground = function getBackground() {
  6004. return this.dom.background;
  6005. };
  6006. /**
  6007. * Get the axis container element
  6008. * @return {HTMLElement} axis
  6009. */
  6010. Group.prototype.getAxis = function getAxis() {
  6011. return this.dom.axis;
  6012. };
  6013. /**
  6014. * Get the width of the group label
  6015. * @return {number} width
  6016. */
  6017. Group.prototype.getLabelWidth = function getLabelWidth() {
  6018. return this.props.label.width;
  6019. };
  6020. /**
  6021. * Repaint this group
  6022. * @param {{start: number, end: number}} range
  6023. * @param {{item: number, axis: number}} margin
  6024. * @param {boolean} [restack=false] Force restacking of all items
  6025. * @return {boolean} Returns true if the group is resized
  6026. */
  6027. Group.prototype.repaint = function repaint(range, margin, restack) {
  6028. var resized = false;
  6029. this.visibleItems = this._updateVisibleItems(this.orderedItems, this.visibleItems, range);
  6030. // force recalculation of the height of the items when the marker height changed
  6031. // (due to the Timeline being attached to the DOM or changed from display:none to visible)
  6032. var markerHeight = this.dom.marker.clientHeight;
  6033. if (markerHeight != this.lastMarkerHeight) {
  6034. this.lastMarkerHeight = markerHeight;
  6035. util.forEach(this.items, function (item) {
  6036. item.dirty = true;
  6037. if (item.displayed) item.repaint();
  6038. });
  6039. restack = true;
  6040. }
  6041. // reposition visible items vertically
  6042. if (this.itemSet.options.stack) { // TODO: ugly way to access options...
  6043. stack.stack(this.visibleItems, margin, restack);
  6044. }
  6045. else { // no stacking
  6046. stack.nostack(this.visibleItems, margin);
  6047. }
  6048. for (var i = 0, ii = this.visibleItems.length; i < ii; i++) {
  6049. var item = this.visibleItems[i];
  6050. item.repositionY();
  6051. }
  6052. // recalculate the height of the group
  6053. var height;
  6054. var visibleItems = this.visibleItems;
  6055. if (visibleItems.length) {
  6056. var min = visibleItems[0].top;
  6057. var max = visibleItems[0].top + visibleItems[0].height;
  6058. util.forEach(visibleItems, function (item) {
  6059. min = Math.min(min, item.top);
  6060. max = Math.max(max, (item.top + item.height));
  6061. });
  6062. height = (max - min) + margin.axis + margin.item;
  6063. }
  6064. else {
  6065. height = margin.axis + margin.item;
  6066. }
  6067. height = Math.max(height, this.props.label.height);
  6068. // calculate actual size and position
  6069. var foreground = this.dom.foreground;
  6070. this.top = foreground.offsetTop;
  6071. this.left = foreground.offsetLeft;
  6072. this.width = foreground.offsetWidth;
  6073. resized = util.updateProperty(this, 'height', height) || resized;
  6074. // recalculate size of label
  6075. resized = util.updateProperty(this.props.label, 'width', this.dom.inner.clientWidth) || resized;
  6076. resized = util.updateProperty(this.props.label, 'height', this.dom.inner.clientHeight) || resized;
  6077. // apply new height
  6078. foreground.style.height = height + 'px';
  6079. this.dom.label.style.height = height + 'px';
  6080. return resized;
  6081. };
  6082. /**
  6083. * Show this group: attach to the DOM
  6084. */
  6085. Group.prototype.show = function show() {
  6086. if (!this.dom.label.parentNode) {
  6087. this.itemSet.getLabelSet().appendChild(this.dom.label);
  6088. }
  6089. if (!this.dom.foreground.parentNode) {
  6090. this.itemSet.getForeground().appendChild(this.dom.foreground);
  6091. }
  6092. if (!this.dom.background.parentNode) {
  6093. this.itemSet.getBackground().appendChild(this.dom.background);
  6094. }
  6095. if (!this.dom.axis.parentNode) {
  6096. this.itemSet.getAxis().appendChild(this.dom.axis);
  6097. }
  6098. };
  6099. /**
  6100. * Hide this group: remove from the DOM
  6101. */
  6102. Group.prototype.hide = function hide() {
  6103. var label = this.dom.label;
  6104. if (label.parentNode) {
  6105. label.parentNode.removeChild(label);
  6106. }
  6107. var foreground = this.dom.foreground;
  6108. if (foreground.parentNode) {
  6109. foreground.parentNode.removeChild(foreground);
  6110. }
  6111. var background = this.dom.background;
  6112. if (background.parentNode) {
  6113. background.parentNode.removeChild(background);
  6114. }
  6115. var axis = this.dom.axis;
  6116. if (axis.parentNode) {
  6117. axis.parentNode.removeChild(axis);
  6118. }
  6119. };
  6120. /**
  6121. * Add an item to the group
  6122. * @param {Item} item
  6123. */
  6124. Group.prototype.add = function add(item) {
  6125. this.items[item.id] = item;
  6126. item.setParent(this);
  6127. if (item instanceof ItemRange && this.visibleItems.indexOf(item) == -1) {
  6128. var range = this.itemSet.range; // TODO: not nice accessing the range like this
  6129. this._checkIfVisible(item, this.visibleItems, range);
  6130. }
  6131. };
  6132. /**
  6133. * Remove an item from the group
  6134. * @param {Item} item
  6135. */
  6136. Group.prototype.remove = function remove(item) {
  6137. delete this.items[item.id];
  6138. item.setParent(this.itemSet);
  6139. // remove from visible items
  6140. var index = this.visibleItems.indexOf(item);
  6141. if (index != -1) this.visibleItems.splice(index, 1);
  6142. // TODO: also remove from ordered items?
  6143. };
  6144. /**
  6145. * Remove an item from the corresponding DataSet
  6146. * @param {Item} item
  6147. */
  6148. Group.prototype.removeFromDataSet = function removeFromDataSet(item) {
  6149. this.itemSet.removeItem(item.id);
  6150. };
  6151. /**
  6152. * Reorder the items
  6153. */
  6154. Group.prototype.order = function order() {
  6155. var array = util.toArray(this.items);
  6156. this.orderedItems.byStart = array;
  6157. this.orderedItems.byEnd = this._constructByEndArray(array);
  6158. stack.orderByStart(this.orderedItems.byStart);
  6159. stack.orderByEnd(this.orderedItems.byEnd);
  6160. };
  6161. /**
  6162. * Create an array containing all items being a range (having an end date)
  6163. * @param {Item[]} array
  6164. * @returns {ItemRange[]}
  6165. * @private
  6166. */
  6167. Group.prototype._constructByEndArray = function _constructByEndArray(array) {
  6168. var endArray = [];
  6169. for (var i = 0; i < array.length; i++) {
  6170. if (array[i] instanceof ItemRange) {
  6171. endArray.push(array[i]);
  6172. }
  6173. }
  6174. return endArray;
  6175. };
  6176. /**
  6177. * Update the visible items
  6178. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems All items ordered by start date and by end date
  6179. * @param {Item[]} visibleItems The previously visible items.
  6180. * @param {{start: number, end: number}} range Visible range
  6181. * @return {Item[]} visibleItems The new visible items.
  6182. * @private
  6183. */
  6184. Group.prototype._updateVisibleItems = function _updateVisibleItems(orderedItems, visibleItems, range) {
  6185. var initialPosByStart,
  6186. newVisibleItems = [],
  6187. i;
  6188. // first check if the items that were in view previously are still in view.
  6189. // this handles the case for the ItemRange that is both before and after the current one.
  6190. if (visibleItems.length > 0) {
  6191. for (i = 0; i < visibleItems.length; i++) {
  6192. this._checkIfVisible(visibleItems[i], newVisibleItems, range);
  6193. }
  6194. }
  6195. // If there were no visible items previously, use binarySearch to find a visible ItemPoint or ItemRange (based on startTime)
  6196. if (newVisibleItems.length == 0) {
  6197. initialPosByStart = this._binarySearch(orderedItems, range, false);
  6198. }
  6199. else {
  6200. initialPosByStart = orderedItems.byStart.indexOf(newVisibleItems[0]);
  6201. }
  6202. // use visible search to find a visible ItemRange (only based on endTime)
  6203. var initialPosByEnd = this._binarySearch(orderedItems, range, true);
  6204. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  6205. if (initialPosByStart != -1) {
  6206. for (i = initialPosByStart; i >= 0; i--) {
  6207. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  6208. }
  6209. for (i = initialPosByStart + 1; i < orderedItems.byStart.length; i++) {
  6210. if (this._checkIfInvisible(orderedItems.byStart[i], newVisibleItems, range)) {break;}
  6211. }
  6212. }
  6213. // if we found a initial ID to use, trace it up and down until we meet an invisible item.
  6214. if (initialPosByEnd != -1) {
  6215. for (i = initialPosByEnd; i >= 0; i--) {
  6216. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  6217. }
  6218. for (i = initialPosByEnd + 1; i < orderedItems.byEnd.length; i++) {
  6219. if (this._checkIfInvisible(orderedItems.byEnd[i], newVisibleItems, range)) {break;}
  6220. }
  6221. }
  6222. return newVisibleItems;
  6223. };
  6224. /**
  6225. * This function does a binary search for a visible item. The user can select either the this.orderedItems.byStart or .byEnd
  6226. * arrays. This is done by giving a boolean value true if you want to use the byEnd.
  6227. * 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
  6228. * if the time we selected (start or end) is within the current range).
  6229. *
  6230. * 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
  6231. * 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,
  6232. * either the start OR end time has to be in the range.
  6233. *
  6234. * @param {{byStart: Item[], byEnd: Item[]}} orderedItems
  6235. * @param {{start: number, end: number}} range
  6236. * @param {Boolean} byEnd
  6237. * @returns {number}
  6238. * @private
  6239. */
  6240. Group.prototype._binarySearch = function _binarySearch(orderedItems, range, byEnd) {
  6241. var array = [];
  6242. var byTime = byEnd ? 'end' : 'start';
  6243. if (byEnd == true) {array = orderedItems.byEnd; }
  6244. else {array = orderedItems.byStart;}
  6245. var interval = range.end - range.start;
  6246. var found = false;
  6247. var low = 0;
  6248. var high = array.length;
  6249. var guess = Math.floor(0.5*(high+low));
  6250. var newGuess;
  6251. if (high == 0) {guess = -1;}
  6252. else if (high == 1) {
  6253. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  6254. guess = 0;
  6255. }
  6256. else {
  6257. guess = -1;
  6258. }
  6259. }
  6260. else {
  6261. high -= 1;
  6262. while (found == false) {
  6263. if ((array[guess].data[byTime] > range.start - interval) && (array[guess].data[byTime] < range.end)) {
  6264. found = true;
  6265. }
  6266. else {
  6267. if (array[guess].data[byTime] < range.start - interval) { // it is too small --> increase low
  6268. low = Math.floor(0.5*(high+low));
  6269. }
  6270. else { // it is too big --> decrease high
  6271. high = Math.floor(0.5*(high+low));
  6272. }
  6273. newGuess = Math.floor(0.5*(high+low));
  6274. // not in list;
  6275. if (guess == newGuess) {
  6276. guess = -1;
  6277. found = true;
  6278. }
  6279. else {
  6280. guess = newGuess;
  6281. }
  6282. }
  6283. }
  6284. }
  6285. return guess;
  6286. };
  6287. /**
  6288. * this function checks if an item is invisible. If it is NOT we make it visible
  6289. * and add it to the global visible items. If it is, return true.
  6290. *
  6291. * @param {Item} item
  6292. * @param {Item[]} visibleItems
  6293. * @param {{start:number, end:number}} range
  6294. * @returns {boolean}
  6295. * @private
  6296. */
  6297. Group.prototype._checkIfInvisible = function _checkIfInvisible(item, visibleItems, range) {
  6298. if (item.isVisible(range)) {
  6299. if (!item.displayed) item.show();
  6300. item.repositionX();
  6301. if (visibleItems.indexOf(item) == -1) {
  6302. visibleItems.push(item);
  6303. }
  6304. return false;
  6305. }
  6306. else {
  6307. return true;
  6308. }
  6309. };
  6310. /**
  6311. * this function is very similar to the _checkIfInvisible() but it does not
  6312. * return booleans, hides the item if it should not be seen and always adds to
  6313. * the visibleItems.
  6314. * this one is for brute forcing and hiding.
  6315. *
  6316. * @param {Item} item
  6317. * @param {Array} visibleItems
  6318. * @param {{start:number, end:number}} range
  6319. * @private
  6320. */
  6321. Group.prototype._checkIfVisible = function _checkIfVisible(item, visibleItems, range) {
  6322. if (item.isVisible(range)) {
  6323. if (!item.displayed) item.show();
  6324. // reposition item horizontally
  6325. item.repositionX();
  6326. visibleItems.push(item);
  6327. }
  6328. else {
  6329. if (item.displayed) item.hide();
  6330. }
  6331. };
  6332. /**
  6333. * Create a timeline visualization
  6334. * @param {HTMLElement} container
  6335. * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
  6336. * @param {Object} [options] See Timeline.setOptions for the available options.
  6337. * @constructor
  6338. */
  6339. function Timeline (container, items, options) {
  6340. // validate arguments
  6341. if (!container) throw new Error('No container element provided');
  6342. var me = this;
  6343. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6344. this.defaultOptions = {
  6345. orientation: 'bottom',
  6346. direction: 'horizontal', // 'horizontal' or 'vertical'
  6347. autoResize: true,
  6348. stack: true,
  6349. editable: {
  6350. updateTime: false,
  6351. updateGroup: false,
  6352. add: false,
  6353. remove: false
  6354. },
  6355. selectable: true,
  6356. start: null,
  6357. end: null,
  6358. min: null,
  6359. max: null,
  6360. zoomMin: 10, // milliseconds
  6361. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6362. // moveable: true, // TODO: option moveable
  6363. // zoomable: true, // TODO: option zoomable
  6364. showMinorLabels: true,
  6365. showMajorLabels: true,
  6366. showCurrentTime: false,
  6367. showCustomTime: false,
  6368. groupOrder: null,
  6369. width: null,
  6370. height: null,
  6371. maxHeight: null,
  6372. minHeight: null,
  6373. type: 'box',
  6374. align: 'center',
  6375. margin: {
  6376. axis: 20,
  6377. item: 10
  6378. },
  6379. padding: 5,
  6380. onAdd: function (item, callback) {
  6381. callback(item);
  6382. },
  6383. onUpdate: function (item, callback) {
  6384. callback(item);
  6385. },
  6386. onMove: function (item, callback) {
  6387. callback(item);
  6388. },
  6389. onRemove: function (item, callback) {
  6390. callback(item);
  6391. }
  6392. };
  6393. this.options = {};
  6394. util.deepExtend(this.options, this.defaultOptions);
  6395. util.deepExtend(this.options, {
  6396. snap: null, // will be specified after timeaxis is created
  6397. toScreen: me._toScreen.bind(me),
  6398. toTime: me._toTime.bind(me)
  6399. });
  6400. // root panel
  6401. var rootOptions = util.extend(Object.create(this.options), {
  6402. height: function () {
  6403. if (me.options.height) {
  6404. // fixed height
  6405. return me.options.height;
  6406. }
  6407. else {
  6408. // auto height
  6409. // TODO: implement a css based solution to automatically have the right hight
  6410. return (me.timeAxis.height + me.contentPanel.height) + 'px';
  6411. }
  6412. }
  6413. });
  6414. this.rootPanel = new RootPanel(container, rootOptions);
  6415. // single select (or unselect) when tapping an item
  6416. this.rootPanel.on('tap', this._onSelectItem.bind(this));
  6417. // multi select when holding mouse/touch, or on ctrl+click
  6418. this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
  6419. // add item on doubletap
  6420. this.rootPanel.on('doubletap', this._onAddItem.bind(this));
  6421. // side panel
  6422. var sideOptions = util.extend(Object.create(this.options), {
  6423. top: function () {
  6424. return (sideOptions.orientation == 'top') ? '0' : '';
  6425. },
  6426. bottom: function () {
  6427. return (sideOptions.orientation == 'top') ? '' : '0';
  6428. },
  6429. left: '0',
  6430. right: null,
  6431. height: '100%',
  6432. width: function () {
  6433. if (me.itemSet) {
  6434. return me.itemSet.getLabelsWidth();
  6435. }
  6436. else {
  6437. return 0;
  6438. }
  6439. },
  6440. className: function () {
  6441. return 'side' + (me.groupsData ? '' : ' hidden');
  6442. }
  6443. });
  6444. this.sidePanel = new Panel(sideOptions);
  6445. this.rootPanel.appendChild(this.sidePanel);
  6446. // main panel (contains time axis and itemsets)
  6447. var mainOptions = util.extend(Object.create(this.options), {
  6448. left: function () {
  6449. // we align left to enable a smooth resizing of the window
  6450. return me.sidePanel.width;
  6451. },
  6452. right: null,
  6453. height: '100%',
  6454. width: function () {
  6455. return me.rootPanel.width - me.sidePanel.width;
  6456. },
  6457. className: 'main'
  6458. });
  6459. this.mainPanel = new Panel(mainOptions);
  6460. this.rootPanel.appendChild(this.mainPanel);
  6461. // range
  6462. // TODO: move range inside rootPanel?
  6463. var rangeOptions = Object.create(this.options);
  6464. this.range = new Range(this.rootPanel, this.mainPanel, rangeOptions);
  6465. this.range.setRange(
  6466. now.clone().add('days', -3).valueOf(),
  6467. now.clone().add('days', 4).valueOf()
  6468. );
  6469. this.range.on('rangechange', function (properties) {
  6470. me.rootPanel.repaint();
  6471. me.emit('rangechange', properties);
  6472. });
  6473. this.range.on('rangechanged', function (properties) {
  6474. me.rootPanel.repaint();
  6475. me.emit('rangechanged', properties);
  6476. });
  6477. // panel with time axis
  6478. var timeAxisOptions = util.extend(Object.create(rootOptions), {
  6479. range: this.range,
  6480. left: null,
  6481. top: null,
  6482. width: null,
  6483. height: null
  6484. });
  6485. this.timeAxis = new TimeAxis(timeAxisOptions);
  6486. this.timeAxis.setRange(this.range);
  6487. this.options.snap = this.timeAxis.snap.bind(this.timeAxis);
  6488. this.mainPanel.appendChild(this.timeAxis);
  6489. // content panel (contains itemset(s))
  6490. var contentOptions = util.extend(Object.create(this.options), {
  6491. top: function () {
  6492. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6493. },
  6494. bottom: function () {
  6495. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6496. },
  6497. left: null,
  6498. right: null,
  6499. height: null,
  6500. width: null,
  6501. className: 'content'
  6502. });
  6503. this.contentPanel = new Panel(contentOptions);
  6504. this.mainPanel.appendChild(this.contentPanel);
  6505. // content panel (contains the vertical lines of box items)
  6506. var backgroundOptions = util.extend(Object.create(this.options), {
  6507. top: function () {
  6508. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6509. },
  6510. bottom: function () {
  6511. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6512. },
  6513. left: null,
  6514. right: null,
  6515. height: function () {
  6516. return me.contentPanel.height;
  6517. },
  6518. width: null,
  6519. className: 'background'
  6520. });
  6521. this.backgroundPanel = new Panel(backgroundOptions);
  6522. this.mainPanel.insertBefore(this.backgroundPanel, this.contentPanel);
  6523. // panel with axis holding the dots of item boxes
  6524. var axisPanelOptions = util.extend(Object.create(rootOptions), {
  6525. left: 0,
  6526. top: function () {
  6527. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6528. },
  6529. bottom: function () {
  6530. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6531. },
  6532. width: '100%',
  6533. height: 0,
  6534. className: 'axis'
  6535. });
  6536. this.axisPanel = new Panel(axisPanelOptions);
  6537. this.mainPanel.appendChild(this.axisPanel);
  6538. // content panel (contains itemset(s))
  6539. var sideContentOptions = util.extend(Object.create(this.options), {
  6540. top: function () {
  6541. return (me.options.orientation == 'top') ? (me.timeAxis.height + 'px') : '';
  6542. },
  6543. bottom: function () {
  6544. return (me.options.orientation == 'top') ? '' : (me.timeAxis.height + 'px');
  6545. },
  6546. left: null,
  6547. right: null,
  6548. height: null,
  6549. width: null,
  6550. className: 'side-content'
  6551. });
  6552. this.sideContentPanel = new Panel(sideContentOptions);
  6553. this.sidePanel.appendChild(this.sideContentPanel);
  6554. // current time bar
  6555. // Note: time bar will be attached in this.setOptions when selected
  6556. this.currentTime = new CurrentTime(this.range, rootOptions);
  6557. // custom time bar
  6558. // Note: time bar will be attached in this.setOptions when selected
  6559. this.customTime = new CustomTime(rootOptions);
  6560. this.customTime.on('timechange', function (time) {
  6561. me.emit('timechange', time);
  6562. });
  6563. this.customTime.on('timechanged', function (time) {
  6564. me.emit('timechanged', time);
  6565. });
  6566. // itemset containing items and groups
  6567. var itemOptions = util.extend(Object.create(this.options), {
  6568. left: null,
  6569. right: null,
  6570. top: null,
  6571. bottom: null,
  6572. width: null,
  6573. height: null
  6574. });
  6575. this.itemSet = new ItemSet(this.backgroundPanel, this.axisPanel, this.sideContentPanel, itemOptions);
  6576. this.itemSet.setRange(this.range);
  6577. this.itemSet.on('change', me.rootPanel.repaint.bind(me.rootPanel));
  6578. this.contentPanel.appendChild(this.itemSet);
  6579. this.itemsData = null; // DataSet
  6580. this.groupsData = null; // DataSet
  6581. // apply options
  6582. if (options) {
  6583. this.setOptions(options);
  6584. }
  6585. // create itemset
  6586. if (items) {
  6587. this.setItems(items);
  6588. }
  6589. }
  6590. // turn Timeline into an event emitter
  6591. Emitter(Timeline.prototype);
  6592. /**
  6593. * Set options
  6594. * @param {Object} options TODO: describe the available options
  6595. */
  6596. Timeline.prototype.setOptions = function (options) {
  6597. util.deepExtend(this.options, options);
  6598. if ('editable' in options) {
  6599. var isBoolean = typeof options.editable === 'boolean';
  6600. this.options.editable = {
  6601. updateTime: isBoolean ? options.editable : (options.editable.updateTime || false),
  6602. updateGroup: isBoolean ? options.editable : (options.editable.updateGroup || false),
  6603. add: isBoolean ? options.editable : (options.editable.add || false),
  6604. remove: isBoolean ? options.editable : (options.editable.remove || false)
  6605. };
  6606. }
  6607. // force update of range (apply new min/max etc.)
  6608. // both start and end are optional
  6609. this.range.setRange(options.start, options.end);
  6610. if ('editable' in options || 'selectable' in options) {
  6611. if (this.options.selectable) {
  6612. // force update of selection
  6613. this.setSelection(this.getSelection());
  6614. }
  6615. else {
  6616. // remove selection
  6617. this.setSelection([]);
  6618. }
  6619. }
  6620. // force the itemSet to refresh: options like orientation and margins may be changed
  6621. this.itemSet.markDirty();
  6622. // validate the callback functions
  6623. var validateCallback = (function (fn) {
  6624. if (!(this.options[fn] instanceof Function) || this.options[fn].length != 2) {
  6625. throw new Error('option ' + fn + ' must be a function ' + fn + '(item, callback)');
  6626. }
  6627. }).bind(this);
  6628. ['onAdd', 'onUpdate', 'onRemove', 'onMove'].forEach(validateCallback);
  6629. // add/remove the current time bar
  6630. if (this.options.showCurrentTime) {
  6631. if (!this.mainPanel.hasChild(this.currentTime)) {
  6632. this.mainPanel.appendChild(this.currentTime);
  6633. this.currentTime.start();
  6634. }
  6635. }
  6636. else {
  6637. if (this.mainPanel.hasChild(this.currentTime)) {
  6638. this.currentTime.stop();
  6639. this.mainPanel.removeChild(this.currentTime);
  6640. }
  6641. }
  6642. // add/remove the custom time bar
  6643. if (this.options.showCustomTime) {
  6644. if (!this.mainPanel.hasChild(this.customTime)) {
  6645. this.mainPanel.appendChild(this.customTime);
  6646. }
  6647. }
  6648. else {
  6649. if (this.mainPanel.hasChild(this.customTime)) {
  6650. this.mainPanel.removeChild(this.customTime);
  6651. }
  6652. }
  6653. // TODO: remove deprecation error one day (deprecated since version 0.8.0)
  6654. if (options && options.order) {
  6655. throw new Error('Option order is deprecated. There is no replacement for this feature.');
  6656. }
  6657. // repaint everything
  6658. this.rootPanel.repaint();
  6659. };
  6660. /**
  6661. * Set a custom time bar
  6662. * @param {Date} time
  6663. */
  6664. Timeline.prototype.setCustomTime = function (time) {
  6665. if (!this.customTime) {
  6666. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6667. }
  6668. this.customTime.setCustomTime(time);
  6669. };
  6670. /**
  6671. * Retrieve the current custom time.
  6672. * @return {Date} customTime
  6673. */
  6674. Timeline.prototype.getCustomTime = function() {
  6675. if (!this.customTime) {
  6676. throw new Error('Cannot get custom time: Custom time bar is not enabled');
  6677. }
  6678. return this.customTime.getCustomTime();
  6679. };
  6680. /**
  6681. * Set items
  6682. * @param {vis.DataSet | Array | google.visualization.DataTable | null} items
  6683. */
  6684. Timeline.prototype.setItems = function(items) {
  6685. var initialLoad = (this.itemsData == null);
  6686. // convert to type DataSet when needed
  6687. var newDataSet;
  6688. if (!items) {
  6689. newDataSet = null;
  6690. }
  6691. else if (items instanceof DataSet || items instanceof DataView) {
  6692. newDataSet = items;
  6693. }
  6694. else {
  6695. // turn an array into a dataset
  6696. newDataSet = new DataSet(items, {
  6697. convert: {
  6698. start: 'Date',
  6699. end: 'Date'
  6700. }
  6701. });
  6702. }
  6703. // set items
  6704. this.itemsData = newDataSet;
  6705. this.itemSet.setItems(newDataSet);
  6706. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6707. this.fit();
  6708. var start = (this.options.start != undefined) ? util.convert(this.options.start, 'Date') : null;
  6709. var end = (this.options.end != undefined) ? util.convert(this.options.end, 'Date') : null;
  6710. this.setWindow(start, end);
  6711. }
  6712. };
  6713. /**
  6714. * Set groups
  6715. * @param {vis.DataSet | Array | google.visualization.DataTable} groups
  6716. */
  6717. Timeline.prototype.setGroups = function setGroups(groups) {
  6718. // convert to type DataSet when needed
  6719. var newDataSet;
  6720. if (!groups) {
  6721. newDataSet = null;
  6722. }
  6723. else if (groups instanceof DataSet || groups instanceof DataView) {
  6724. newDataSet = groups;
  6725. }
  6726. else {
  6727. // turn an array into a dataset
  6728. newDataSet = new DataSet(groups);
  6729. }
  6730. this.groupsData = newDataSet;
  6731. this.itemSet.setGroups(newDataSet);
  6732. };
  6733. /**
  6734. * Clear the Timeline. By Default, items, groups and options are cleared.
  6735. * Example usage:
  6736. *
  6737. * timeline.clear(); // clear items, groups, and options
  6738. * timeline.clear({options: true}); // clear options only
  6739. *
  6740. * @param {Object} [what] Optionally specify what to clear. By default:
  6741. * {items: true, groups: true, options: true}
  6742. */
  6743. Timeline.prototype.clear = function clear(what) {
  6744. // clear items
  6745. if (!what || what.items) {
  6746. this.setItems(null);
  6747. }
  6748. // clear groups
  6749. if (!what || what.groups) {
  6750. this.setGroups(null);
  6751. }
  6752. // clear options
  6753. if (!what || what.options) {
  6754. this.setOptions(this.defaultOptions);
  6755. }
  6756. };
  6757. /**
  6758. * Set Timeline window such that it fits all items
  6759. */
  6760. Timeline.prototype.fit = function fit() {
  6761. // apply the data range as range
  6762. var dataRange = this.getItemRange();
  6763. // add 5% space on both sides
  6764. var start = dataRange.min;
  6765. var end = dataRange.max;
  6766. if (start != null && end != null) {
  6767. var interval = (end.valueOf() - start.valueOf());
  6768. if (interval <= 0) {
  6769. // prevent an empty interval
  6770. interval = 24 * 60 * 60 * 1000; // 1 day
  6771. }
  6772. start = new Date(start.valueOf() - interval * 0.05);
  6773. end = new Date(end.valueOf() + interval * 0.05);
  6774. }
  6775. // skip range set if there is no start and end date
  6776. if (start === null && end === null) {
  6777. return;
  6778. }
  6779. this.range.setRange(start, end);
  6780. };
  6781. /**
  6782. * Get the data range of the item set.
  6783. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6784. * When no minimum is found, min==null
  6785. * When no maximum is found, max==null
  6786. */
  6787. Timeline.prototype.getItemRange = function getItemRange() {
  6788. // calculate min from start filed
  6789. var itemsData = this.itemsData,
  6790. min = null,
  6791. max = null;
  6792. if (itemsData) {
  6793. // calculate the minimum value of the field 'start'
  6794. var minItem = itemsData.min('start');
  6795. min = minItem ? minItem.start.valueOf() : null;
  6796. // calculate maximum value of fields 'start' and 'end'
  6797. var maxStartItem = itemsData.max('start');
  6798. if (maxStartItem) {
  6799. max = maxStartItem.start.valueOf();
  6800. }
  6801. var maxEndItem = itemsData.max('end');
  6802. if (maxEndItem) {
  6803. if (max == null) {
  6804. max = maxEndItem.end.valueOf();
  6805. }
  6806. else {
  6807. max = Math.max(max, maxEndItem.end.valueOf());
  6808. }
  6809. }
  6810. }
  6811. return {
  6812. min: (min != null) ? new Date(min) : null,
  6813. max: (max != null) ? new Date(max) : null
  6814. };
  6815. };
  6816. /**
  6817. * Set selected items by their id. Replaces the current selection
  6818. * Unknown id's are silently ignored.
  6819. * @param {Array} [ids] An array with zero or more id's of the items to be
  6820. * selected. If ids is an empty array, all items will be
  6821. * unselected.
  6822. */
  6823. Timeline.prototype.setSelection = function setSelection (ids) {
  6824. this.itemSet.setSelection(ids);
  6825. };
  6826. /**
  6827. * Get the selected items by their id
  6828. * @return {Array} ids The ids of the selected items
  6829. */
  6830. Timeline.prototype.getSelection = function getSelection() {
  6831. return this.itemSet.getSelection();
  6832. };
  6833. /**
  6834. * Set the visible window. Both parameters are optional, you can change only
  6835. * start or only end. Syntax:
  6836. *
  6837. * TimeLine.setWindow(start, end)
  6838. * TimeLine.setWindow(range)
  6839. *
  6840. * Where start and end can be a Date, number, or string, and range is an
  6841. * object with properties start and end.
  6842. *
  6843. * @param {Date | Number | String | Object} [start] Start date of visible window
  6844. * @param {Date | Number | String} [end] End date of visible window
  6845. */
  6846. Timeline.prototype.setWindow = function setWindow(start, end) {
  6847. if (arguments.length == 1) {
  6848. var range = arguments[0];
  6849. this.range.setRange(range.start, range.end);
  6850. }
  6851. else {
  6852. this.range.setRange(start, end);
  6853. }
  6854. };
  6855. /**
  6856. * Get the visible window
  6857. * @return {{start: Date, end: Date}} Visible range
  6858. */
  6859. Timeline.prototype.getWindow = function setWindow() {
  6860. var range = this.range.getRange();
  6861. return {
  6862. start: new Date(range.start),
  6863. end: new Date(range.end)
  6864. };
  6865. };
  6866. /**
  6867. * Force a redraw of the Timeline. Can be useful to manually redraw when
  6868. * option autoResize=false
  6869. */
  6870. Timeline.prototype.redraw = function redraw() {
  6871. this.rootPanel.repaint();
  6872. };
  6873. // TODO: deprecated since version 1.1.0, remove some day
  6874. Timeline.prototype.repaint = function repaint() {
  6875. throw new Error('Function repaint is deprecated. Use redraw instead.');
  6876. };
  6877. /**
  6878. * Handle selecting/deselecting an item when tapping it
  6879. * @param {Event} event
  6880. * @private
  6881. */
  6882. // TODO: move this function to ItemSet
  6883. Timeline.prototype._onSelectItem = function (event) {
  6884. if (!this.options.selectable) return;
  6885. var ctrlKey = event.gesture.srcEvent && event.gesture.srcEvent.ctrlKey;
  6886. var shiftKey = event.gesture.srcEvent && event.gesture.srcEvent.shiftKey;
  6887. if (ctrlKey || shiftKey) {
  6888. this._onMultiSelectItem(event);
  6889. return;
  6890. }
  6891. var oldSelection = this.getSelection();
  6892. var item = ItemSet.itemFromTarget(event);
  6893. var selection = item ? [item.id] : [];
  6894. this.setSelection(selection);
  6895. var newSelection = this.getSelection();
  6896. // emit a select event,
  6897. // except when old selection is empty and new selection is still empty
  6898. if (newSelection.length > 0 || oldSelection.length > 0) {
  6899. this.emit('select', {
  6900. items: this.getSelection()
  6901. });
  6902. }
  6903. event.stopPropagation();
  6904. };
  6905. /**
  6906. * Handle creation and updates of an item on double tap
  6907. * @param event
  6908. * @private
  6909. */
  6910. Timeline.prototype._onAddItem = function (event) {
  6911. if (!this.options.selectable) return;
  6912. if (!this.options.editable.add) return;
  6913. var me = this,
  6914. item = ItemSet.itemFromTarget(event);
  6915. if (item) {
  6916. // update item
  6917. // execute async handler to update the item (or cancel it)
  6918. var itemData = me.itemsData.get(item.id); // get a clone of the data from the dataset
  6919. this.options.onUpdate(itemData, function (itemData) {
  6920. if (itemData) {
  6921. me.itemsData.update(itemData);
  6922. }
  6923. });
  6924. }
  6925. else {
  6926. // add item
  6927. var xAbs = vis.util.getAbsoluteLeft(this.contentPanel.frame);
  6928. var x = event.gesture.center.pageX - xAbs;
  6929. var newItem = {
  6930. start: this.timeAxis.snap(this._toTime(x)),
  6931. content: 'new item'
  6932. };
  6933. // when default type is a range, add a default end date to the new item
  6934. if (this.options.type === 'range' || this.options.type == 'rangeoverflow') {
  6935. newItem.end = this.timeAxis.snap(this._toTime(x + this.rootPanel.width / 5));
  6936. }
  6937. var id = util.randomUUID();
  6938. newItem[this.itemsData.fieldId] = id;
  6939. var group = ItemSet.groupFromTarget(event);
  6940. if (group) {
  6941. newItem.group = group.groupId;
  6942. }
  6943. // execute async handler to customize (or cancel) adding an item
  6944. this.options.onAdd(newItem, function (item) {
  6945. if (item) {
  6946. me.itemsData.add(newItem);
  6947. // TODO: need to trigger a redraw?
  6948. }
  6949. });
  6950. }
  6951. };
  6952. /**
  6953. * Handle selecting/deselecting multiple items when holding an item
  6954. * @param {Event} event
  6955. * @private
  6956. */
  6957. // TODO: move this function to ItemSet
  6958. Timeline.prototype._onMultiSelectItem = function (event) {
  6959. if (!this.options.selectable) return;
  6960. var selection,
  6961. item = ItemSet.itemFromTarget(event);
  6962. if (item) {
  6963. // multi select items
  6964. selection = this.getSelection(); // current selection
  6965. var index = selection.indexOf(item.id);
  6966. if (index == -1) {
  6967. // item is not yet selected -> select it
  6968. selection.push(item.id);
  6969. }
  6970. else {
  6971. // item is already selected -> deselect it
  6972. selection.splice(index, 1);
  6973. }
  6974. this.setSelection(selection);
  6975. this.emit('select', {
  6976. items: this.getSelection()
  6977. });
  6978. event.stopPropagation();
  6979. }
  6980. };
  6981. /**
  6982. * Convert a position on screen (pixels) to a datetime
  6983. * @param {int} x Position on the screen in pixels
  6984. * @return {Date} time The datetime the corresponds with given position x
  6985. * @private
  6986. */
  6987. Timeline.prototype._toTime = function _toTime(x) {
  6988. var conversion = this.range.conversion(this.mainPanel.width);
  6989. return new Date(x / conversion.scale + conversion.offset);
  6990. };
  6991. /**
  6992. * Convert a datetime (Date object) into a position on the screen
  6993. * @param {Date} time A date
  6994. * @return {int} x The position on the screen in pixels which corresponds
  6995. * with the given date.
  6996. * @private
  6997. */
  6998. Timeline.prototype._toScreen = function _toScreen(time) {
  6999. var conversion = this.range.conversion(this.mainPanel.width);
  7000. return (time.valueOf() - conversion.offset) * conversion.scale;
  7001. };
  7002. (function(exports) {
  7003. /**
  7004. * Parse a text source containing data in DOT language into a JSON object.
  7005. * The object contains two lists: one with nodes and one with edges.
  7006. *
  7007. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  7008. *
  7009. * @param {String} data Text containing a graph in DOT-notation
  7010. * @return {Object} graph An object containing two parameters:
  7011. * {Object[]} nodes
  7012. * {Object[]} edges
  7013. */
  7014. function parseDOT (data) {
  7015. dot = data;
  7016. return parseGraph();
  7017. }
  7018. // token types enumeration
  7019. var TOKENTYPE = {
  7020. NULL : 0,
  7021. DELIMITER : 1,
  7022. IDENTIFIER: 2,
  7023. UNKNOWN : 3
  7024. };
  7025. // map with all delimiters
  7026. var DELIMITERS = {
  7027. '{': true,
  7028. '}': true,
  7029. '[': true,
  7030. ']': true,
  7031. ';': true,
  7032. '=': true,
  7033. ',': true,
  7034. '->': true,
  7035. '--': true
  7036. };
  7037. var dot = ''; // current dot file
  7038. var index = 0; // current index in dot file
  7039. var c = ''; // current token character in expr
  7040. var token = ''; // current token
  7041. var tokenType = TOKENTYPE.NULL; // type of the token
  7042. /**
  7043. * Get the first character from the dot file.
  7044. * The character is stored into the char c. If the end of the dot file is
  7045. * reached, the function puts an empty string in c.
  7046. */
  7047. function first() {
  7048. index = 0;
  7049. c = dot.charAt(0);
  7050. }
  7051. /**
  7052. * Get the next character from the dot file.
  7053. * The character is stored into the char c. If the end of the dot file is
  7054. * reached, the function puts an empty string in c.
  7055. */
  7056. function next() {
  7057. index++;
  7058. c = dot.charAt(index);
  7059. }
  7060. /**
  7061. * Preview the next character from the dot file.
  7062. * @return {String} cNext
  7063. */
  7064. function nextPreview() {
  7065. return dot.charAt(index + 1);
  7066. }
  7067. /**
  7068. * Test whether given character is alphabetic or numeric
  7069. * @param {String} c
  7070. * @return {Boolean} isAlphaNumeric
  7071. */
  7072. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  7073. function isAlphaNumeric(c) {
  7074. return regexAlphaNumeric.test(c);
  7075. }
  7076. /**
  7077. * Merge all properties of object b into object b
  7078. * @param {Object} a
  7079. * @param {Object} b
  7080. * @return {Object} a
  7081. */
  7082. function merge (a, b) {
  7083. if (!a) {
  7084. a = {};
  7085. }
  7086. if (b) {
  7087. for (var name in b) {
  7088. if (b.hasOwnProperty(name)) {
  7089. a[name] = b[name];
  7090. }
  7091. }
  7092. }
  7093. return a;
  7094. }
  7095. /**
  7096. * Set a value in an object, where the provided parameter name can be a
  7097. * path with nested parameters. For example:
  7098. *
  7099. * var obj = {a: 2};
  7100. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  7101. *
  7102. * @param {Object} obj
  7103. * @param {String} path A parameter name or dot-separated parameter path,
  7104. * like "color.highlight.border".
  7105. * @param {*} value
  7106. */
  7107. function setValue(obj, path, value) {
  7108. var keys = path.split('.');
  7109. var o = obj;
  7110. while (keys.length) {
  7111. var key = keys.shift();
  7112. if (keys.length) {
  7113. // this isn't the end point
  7114. if (!o[key]) {
  7115. o[key] = {};
  7116. }
  7117. o = o[key];
  7118. }
  7119. else {
  7120. // this is the end point
  7121. o[key] = value;
  7122. }
  7123. }
  7124. }
  7125. /**
  7126. * Add a node to a graph object. If there is already a node with
  7127. * the same id, their attributes will be merged.
  7128. * @param {Object} graph
  7129. * @param {Object} node
  7130. */
  7131. function addNode(graph, node) {
  7132. var i, len;
  7133. var current = null;
  7134. // find root graph (in case of subgraph)
  7135. var graphs = [graph]; // list with all graphs from current graph to root graph
  7136. var root = graph;
  7137. while (root.parent) {
  7138. graphs.push(root.parent);
  7139. root = root.parent;
  7140. }
  7141. // find existing node (at root level) by its id
  7142. if (root.nodes) {
  7143. for (i = 0, len = root.nodes.length; i < len; i++) {
  7144. if (node.id === root.nodes[i].id) {
  7145. current = root.nodes[i];
  7146. break;
  7147. }
  7148. }
  7149. }
  7150. if (!current) {
  7151. // this is a new node
  7152. current = {
  7153. id: node.id
  7154. };
  7155. if (graph.node) {
  7156. // clone default attributes
  7157. current.attr = merge(current.attr, graph.node);
  7158. }
  7159. }
  7160. // add node to this (sub)graph and all its parent graphs
  7161. for (i = graphs.length - 1; i >= 0; i--) {
  7162. var g = graphs[i];
  7163. if (!g.nodes) {
  7164. g.nodes = [];
  7165. }
  7166. if (g.nodes.indexOf(current) == -1) {
  7167. g.nodes.push(current);
  7168. }
  7169. }
  7170. // merge attributes
  7171. if (node.attr) {
  7172. current.attr = merge(current.attr, node.attr);
  7173. }
  7174. }
  7175. /**
  7176. * Add an edge to a graph object
  7177. * @param {Object} graph
  7178. * @param {Object} edge
  7179. */
  7180. function addEdge(graph, edge) {
  7181. if (!graph.edges) {
  7182. graph.edges = [];
  7183. }
  7184. graph.edges.push(edge);
  7185. if (graph.edge) {
  7186. var attr = merge({}, graph.edge); // clone default attributes
  7187. edge.attr = merge(attr, edge.attr); // merge attributes
  7188. }
  7189. }
  7190. /**
  7191. * Create an edge to a graph object
  7192. * @param {Object} graph
  7193. * @param {String | Number | Object} from
  7194. * @param {String | Number | Object} to
  7195. * @param {String} type
  7196. * @param {Object | null} attr
  7197. * @return {Object} edge
  7198. */
  7199. function createEdge(graph, from, to, type, attr) {
  7200. var edge = {
  7201. from: from,
  7202. to: to,
  7203. type: type
  7204. };
  7205. if (graph.edge) {
  7206. edge.attr = merge({}, graph.edge); // clone default attributes
  7207. }
  7208. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  7209. return edge;
  7210. }
  7211. /**
  7212. * Get next token in the current dot file.
  7213. * The token and token type are available as token and tokenType
  7214. */
  7215. function getToken() {
  7216. tokenType = TOKENTYPE.NULL;
  7217. token = '';
  7218. // skip over whitespaces
  7219. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7220. next();
  7221. }
  7222. do {
  7223. var isComment = false;
  7224. // skip comment
  7225. if (c == '#') {
  7226. // find the previous non-space character
  7227. var i = index - 1;
  7228. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7229. i--;
  7230. }
  7231. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7232. // the # is at the start of a line, this is indeed a line comment
  7233. while (c != '' && c != '\n') {
  7234. next();
  7235. }
  7236. isComment = true;
  7237. }
  7238. }
  7239. if (c == '/' && nextPreview() == '/') {
  7240. // skip line comment
  7241. while (c != '' && c != '\n') {
  7242. next();
  7243. }
  7244. isComment = true;
  7245. }
  7246. if (c == '/' && nextPreview() == '*') {
  7247. // skip block comment
  7248. while (c != '') {
  7249. if (c == '*' && nextPreview() == '/') {
  7250. // end of block comment found. skip these last two characters
  7251. next();
  7252. next();
  7253. break;
  7254. }
  7255. else {
  7256. next();
  7257. }
  7258. }
  7259. isComment = true;
  7260. }
  7261. // skip over whitespaces
  7262. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7263. next();
  7264. }
  7265. }
  7266. while (isComment);
  7267. // check for end of dot file
  7268. if (c == '') {
  7269. // token is still empty
  7270. tokenType = TOKENTYPE.DELIMITER;
  7271. return;
  7272. }
  7273. // check for delimiters consisting of 2 characters
  7274. var c2 = c + nextPreview();
  7275. if (DELIMITERS[c2]) {
  7276. tokenType = TOKENTYPE.DELIMITER;
  7277. token = c2;
  7278. next();
  7279. next();
  7280. return;
  7281. }
  7282. // check for delimiters consisting of 1 character
  7283. if (DELIMITERS[c]) {
  7284. tokenType = TOKENTYPE.DELIMITER;
  7285. token = c;
  7286. next();
  7287. return;
  7288. }
  7289. // check for an identifier (number or string)
  7290. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7291. if (isAlphaNumeric(c) || c == '-') {
  7292. token += c;
  7293. next();
  7294. while (isAlphaNumeric(c)) {
  7295. token += c;
  7296. next();
  7297. }
  7298. if (token == 'false') {
  7299. token = false; // convert to boolean
  7300. }
  7301. else if (token == 'true') {
  7302. token = true; // convert to boolean
  7303. }
  7304. else if (!isNaN(Number(token))) {
  7305. token = Number(token); // convert to number
  7306. }
  7307. tokenType = TOKENTYPE.IDENTIFIER;
  7308. return;
  7309. }
  7310. // check for a string enclosed by double quotes
  7311. if (c == '"') {
  7312. next();
  7313. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7314. token += c;
  7315. if (c == '"') { // skip the escape character
  7316. next();
  7317. }
  7318. next();
  7319. }
  7320. if (c != '"') {
  7321. throw newSyntaxError('End of string " expected');
  7322. }
  7323. next();
  7324. tokenType = TOKENTYPE.IDENTIFIER;
  7325. return;
  7326. }
  7327. // something unknown is found, wrong characters, a syntax error
  7328. tokenType = TOKENTYPE.UNKNOWN;
  7329. while (c != '') {
  7330. token += c;
  7331. next();
  7332. }
  7333. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7334. }
  7335. /**
  7336. * Parse a graph.
  7337. * @returns {Object} graph
  7338. */
  7339. function parseGraph() {
  7340. var graph = {};
  7341. first();
  7342. getToken();
  7343. // optional strict keyword
  7344. if (token == 'strict') {
  7345. graph.strict = true;
  7346. getToken();
  7347. }
  7348. // graph or digraph keyword
  7349. if (token == 'graph' || token == 'digraph') {
  7350. graph.type = token;
  7351. getToken();
  7352. }
  7353. // optional graph id
  7354. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7355. graph.id = token;
  7356. getToken();
  7357. }
  7358. // open angle bracket
  7359. if (token != '{') {
  7360. throw newSyntaxError('Angle bracket { expected');
  7361. }
  7362. getToken();
  7363. // statements
  7364. parseStatements(graph);
  7365. // close angle bracket
  7366. if (token != '}') {
  7367. throw newSyntaxError('Angle bracket } expected');
  7368. }
  7369. getToken();
  7370. // end of file
  7371. if (token !== '') {
  7372. throw newSyntaxError('End of file expected');
  7373. }
  7374. getToken();
  7375. // remove temporary default properties
  7376. delete graph.node;
  7377. delete graph.edge;
  7378. delete graph.graph;
  7379. return graph;
  7380. }
  7381. /**
  7382. * Parse a list with statements.
  7383. * @param {Object} graph
  7384. */
  7385. function parseStatements (graph) {
  7386. while (token !== '' && token != '}') {
  7387. parseStatement(graph);
  7388. if (token == ';') {
  7389. getToken();
  7390. }
  7391. }
  7392. }
  7393. /**
  7394. * Parse a single statement. Can be a an attribute statement, node
  7395. * statement, a series of node statements and edge statements, or a
  7396. * parameter.
  7397. * @param {Object} graph
  7398. */
  7399. function parseStatement(graph) {
  7400. // parse subgraph
  7401. var subgraph = parseSubgraph(graph);
  7402. if (subgraph) {
  7403. // edge statements
  7404. parseEdge(graph, subgraph);
  7405. return;
  7406. }
  7407. // parse an attribute statement
  7408. var attr = parseAttributeStatement(graph);
  7409. if (attr) {
  7410. return;
  7411. }
  7412. // parse node
  7413. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7414. throw newSyntaxError('Identifier expected');
  7415. }
  7416. var id = token; // id can be a string or a number
  7417. getToken();
  7418. if (token == '=') {
  7419. // id statement
  7420. getToken();
  7421. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7422. throw newSyntaxError('Identifier expected');
  7423. }
  7424. graph[id] = token;
  7425. getToken();
  7426. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7427. }
  7428. else {
  7429. parseNodeStatement(graph, id);
  7430. }
  7431. }
  7432. /**
  7433. * Parse a subgraph
  7434. * @param {Object} graph parent graph object
  7435. * @return {Object | null} subgraph
  7436. */
  7437. function parseSubgraph (graph) {
  7438. var subgraph = null;
  7439. // optional subgraph keyword
  7440. if (token == 'subgraph') {
  7441. subgraph = {};
  7442. subgraph.type = 'subgraph';
  7443. getToken();
  7444. // optional graph id
  7445. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7446. subgraph.id = token;
  7447. getToken();
  7448. }
  7449. }
  7450. // open angle bracket
  7451. if (token == '{') {
  7452. getToken();
  7453. if (!subgraph) {
  7454. subgraph = {};
  7455. }
  7456. subgraph.parent = graph;
  7457. subgraph.node = graph.node;
  7458. subgraph.edge = graph.edge;
  7459. subgraph.graph = graph.graph;
  7460. // statements
  7461. parseStatements(subgraph);
  7462. // close angle bracket
  7463. if (token != '}') {
  7464. throw newSyntaxError('Angle bracket } expected');
  7465. }
  7466. getToken();
  7467. // remove temporary default properties
  7468. delete subgraph.node;
  7469. delete subgraph.edge;
  7470. delete subgraph.graph;
  7471. delete subgraph.parent;
  7472. // register at the parent graph
  7473. if (!graph.subgraphs) {
  7474. graph.subgraphs = [];
  7475. }
  7476. graph.subgraphs.push(subgraph);
  7477. }
  7478. return subgraph;
  7479. }
  7480. /**
  7481. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7482. * Available keywords are 'node', 'edge', 'graph'.
  7483. * The previous list with default attributes will be replaced
  7484. * @param {Object} graph
  7485. * @returns {String | null} keyword Returns the name of the parsed attribute
  7486. * (node, edge, graph), or null if nothing
  7487. * is parsed.
  7488. */
  7489. function parseAttributeStatement (graph) {
  7490. // attribute statements
  7491. if (token == 'node') {
  7492. getToken();
  7493. // node attributes
  7494. graph.node = parseAttributeList();
  7495. return 'node';
  7496. }
  7497. else if (token == 'edge') {
  7498. getToken();
  7499. // edge attributes
  7500. graph.edge = parseAttributeList();
  7501. return 'edge';
  7502. }
  7503. else if (token == 'graph') {
  7504. getToken();
  7505. // graph attributes
  7506. graph.graph = parseAttributeList();
  7507. return 'graph';
  7508. }
  7509. return null;
  7510. }
  7511. /**
  7512. * parse a node statement
  7513. * @param {Object} graph
  7514. * @param {String | Number} id
  7515. */
  7516. function parseNodeStatement(graph, id) {
  7517. // node statement
  7518. var node = {
  7519. id: id
  7520. };
  7521. var attr = parseAttributeList();
  7522. if (attr) {
  7523. node.attr = attr;
  7524. }
  7525. addNode(graph, node);
  7526. // edge statements
  7527. parseEdge(graph, id);
  7528. }
  7529. /**
  7530. * Parse an edge or a series of edges
  7531. * @param {Object} graph
  7532. * @param {String | Number} from Id of the from node
  7533. */
  7534. function parseEdge(graph, from) {
  7535. while (token == '->' || token == '--') {
  7536. var to;
  7537. var type = token;
  7538. getToken();
  7539. var subgraph = parseSubgraph(graph);
  7540. if (subgraph) {
  7541. to = subgraph;
  7542. }
  7543. else {
  7544. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7545. throw newSyntaxError('Identifier or subgraph expected');
  7546. }
  7547. to = token;
  7548. addNode(graph, {
  7549. id: to
  7550. });
  7551. getToken();
  7552. }
  7553. // parse edge attributes
  7554. var attr = parseAttributeList();
  7555. // create edge
  7556. var edge = createEdge(graph, from, to, type, attr);
  7557. addEdge(graph, edge);
  7558. from = to;
  7559. }
  7560. }
  7561. /**
  7562. * Parse a set with attributes,
  7563. * for example [label="1.000", shape=solid]
  7564. * @return {Object | null} attr
  7565. */
  7566. function parseAttributeList() {
  7567. var attr = null;
  7568. while (token == '[') {
  7569. getToken();
  7570. attr = {};
  7571. while (token !== '' && token != ']') {
  7572. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7573. throw newSyntaxError('Attribute name expected');
  7574. }
  7575. var name = token;
  7576. getToken();
  7577. if (token != '=') {
  7578. throw newSyntaxError('Equal sign = expected');
  7579. }
  7580. getToken();
  7581. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7582. throw newSyntaxError('Attribute value expected');
  7583. }
  7584. var value = token;
  7585. setValue(attr, name, value); // name can be a path
  7586. getToken();
  7587. if (token ==',') {
  7588. getToken();
  7589. }
  7590. }
  7591. if (token != ']') {
  7592. throw newSyntaxError('Bracket ] expected');
  7593. }
  7594. getToken();
  7595. }
  7596. return attr;
  7597. }
  7598. /**
  7599. * Create a syntax error with extra information on current token and index.
  7600. * @param {String} message
  7601. * @returns {SyntaxError} err
  7602. */
  7603. function newSyntaxError(message) {
  7604. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7605. }
  7606. /**
  7607. * Chop off text after a maximum length
  7608. * @param {String} text
  7609. * @param {Number} maxLength
  7610. * @returns {String}
  7611. */
  7612. function chop (text, maxLength) {
  7613. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7614. }
  7615. /**
  7616. * Execute a function fn for each pair of elements in two arrays
  7617. * @param {Array | *} array1
  7618. * @param {Array | *} array2
  7619. * @param {function} fn
  7620. */
  7621. function forEach2(array1, array2, fn) {
  7622. if (array1 instanceof Array) {
  7623. array1.forEach(function (elem1) {
  7624. if (array2 instanceof Array) {
  7625. array2.forEach(function (elem2) {
  7626. fn(elem1, elem2);
  7627. });
  7628. }
  7629. else {
  7630. fn(elem1, array2);
  7631. }
  7632. });
  7633. }
  7634. else {
  7635. if (array2 instanceof Array) {
  7636. array2.forEach(function (elem2) {
  7637. fn(array1, elem2);
  7638. });
  7639. }
  7640. else {
  7641. fn(array1, array2);
  7642. }
  7643. }
  7644. }
  7645. /**
  7646. * Convert a string containing a graph in DOT language into a map containing
  7647. * with nodes and edges in the format of graph.
  7648. * @param {String} data Text containing a graph in DOT-notation
  7649. * @return {Object} graphData
  7650. */
  7651. function DOTToGraph (data) {
  7652. // parse the DOT file
  7653. var dotData = parseDOT(data);
  7654. var graphData = {
  7655. nodes: [],
  7656. edges: [],
  7657. options: {}
  7658. };
  7659. // copy the nodes
  7660. if (dotData.nodes) {
  7661. dotData.nodes.forEach(function (dotNode) {
  7662. var graphNode = {
  7663. id: dotNode.id,
  7664. label: String(dotNode.label || dotNode.id)
  7665. };
  7666. merge(graphNode, dotNode.attr);
  7667. if (graphNode.image) {
  7668. graphNode.shape = 'image';
  7669. }
  7670. graphData.nodes.push(graphNode);
  7671. });
  7672. }
  7673. // copy the edges
  7674. if (dotData.edges) {
  7675. /**
  7676. * Convert an edge in DOT format to an edge with VisGraph format
  7677. * @param {Object} dotEdge
  7678. * @returns {Object} graphEdge
  7679. */
  7680. function convertEdge(dotEdge) {
  7681. var graphEdge = {
  7682. from: dotEdge.from,
  7683. to: dotEdge.to
  7684. };
  7685. merge(graphEdge, dotEdge.attr);
  7686. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7687. return graphEdge;
  7688. }
  7689. dotData.edges.forEach(function (dotEdge) {
  7690. var from, to;
  7691. if (dotEdge.from instanceof Object) {
  7692. from = dotEdge.from.nodes;
  7693. }
  7694. else {
  7695. from = {
  7696. id: dotEdge.from
  7697. }
  7698. }
  7699. if (dotEdge.to instanceof Object) {
  7700. to = dotEdge.to.nodes;
  7701. }
  7702. else {
  7703. to = {
  7704. id: dotEdge.to
  7705. }
  7706. }
  7707. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7708. dotEdge.from.edges.forEach(function (subEdge) {
  7709. var graphEdge = convertEdge(subEdge);
  7710. graphData.edges.push(graphEdge);
  7711. });
  7712. }
  7713. forEach2(from, to, function (from, to) {
  7714. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7715. var graphEdge = convertEdge(subEdge);
  7716. graphData.edges.push(graphEdge);
  7717. });
  7718. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7719. dotEdge.to.edges.forEach(function (subEdge) {
  7720. var graphEdge = convertEdge(subEdge);
  7721. graphData.edges.push(graphEdge);
  7722. });
  7723. }
  7724. });
  7725. }
  7726. // copy the options
  7727. if (dotData.attr) {
  7728. graphData.options = dotData.attr;
  7729. }
  7730. return graphData;
  7731. }
  7732. // exports
  7733. exports.parseDOT = parseDOT;
  7734. exports.DOTToGraph = DOTToGraph;
  7735. })(typeof util !== 'undefined' ? util : exports);
  7736. /**
  7737. * Canvas shapes used by the Graph
  7738. */
  7739. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7740. /**
  7741. * Draw a circle shape
  7742. */
  7743. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7744. this.beginPath();
  7745. this.arc(x, y, r, 0, 2*Math.PI, false);
  7746. };
  7747. /**
  7748. * Draw a square shape
  7749. * @param {Number} x horizontal center
  7750. * @param {Number} y vertical center
  7751. * @param {Number} r size, width and height of the square
  7752. */
  7753. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7754. this.beginPath();
  7755. this.rect(x - r, y - r, r * 2, r * 2);
  7756. };
  7757. /**
  7758. * Draw a triangle shape
  7759. * @param {Number} x horizontal center
  7760. * @param {Number} y vertical center
  7761. * @param {Number} r radius, half the length of the sides of the triangle
  7762. */
  7763. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7764. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7765. this.beginPath();
  7766. var s = r * 2;
  7767. var s2 = s / 2;
  7768. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7769. var h = Math.sqrt(s * s - s2 * s2); // height
  7770. this.moveTo(x, y - (h - ir));
  7771. this.lineTo(x + s2, y + ir);
  7772. this.lineTo(x - s2, y + ir);
  7773. this.lineTo(x, y - (h - ir));
  7774. this.closePath();
  7775. };
  7776. /**
  7777. * Draw a triangle shape in downward orientation
  7778. * @param {Number} x horizontal center
  7779. * @param {Number} y vertical center
  7780. * @param {Number} r radius
  7781. */
  7782. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7783. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7784. this.beginPath();
  7785. var s = r * 2;
  7786. var s2 = s / 2;
  7787. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7788. var h = Math.sqrt(s * s - s2 * s2); // height
  7789. this.moveTo(x, y + (h - ir));
  7790. this.lineTo(x + s2, y - ir);
  7791. this.lineTo(x - s2, y - ir);
  7792. this.lineTo(x, y + (h - ir));
  7793. this.closePath();
  7794. };
  7795. /**
  7796. * Draw a star shape, a star with 5 points
  7797. * @param {Number} x horizontal center
  7798. * @param {Number} y vertical center
  7799. * @param {Number} r radius, half the length of the sides of the triangle
  7800. */
  7801. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7802. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7803. this.beginPath();
  7804. for (var n = 0; n < 10; n++) {
  7805. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7806. this.lineTo(
  7807. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7808. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7809. );
  7810. }
  7811. this.closePath();
  7812. };
  7813. /**
  7814. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7815. */
  7816. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7817. var r2d = Math.PI/180;
  7818. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7819. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7820. this.beginPath();
  7821. this.moveTo(x+r,y);
  7822. this.lineTo(x+w-r,y);
  7823. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7824. this.lineTo(x+w,y+h-r);
  7825. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7826. this.lineTo(x+r,y+h);
  7827. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7828. this.lineTo(x,y+r);
  7829. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7830. };
  7831. /**
  7832. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7833. */
  7834. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7835. var kappa = .5522848,
  7836. ox = (w / 2) * kappa, // control point offset horizontal
  7837. oy = (h / 2) * kappa, // control point offset vertical
  7838. xe = x + w, // x-end
  7839. ye = y + h, // y-end
  7840. xm = x + w / 2, // x-middle
  7841. ym = y + h / 2; // y-middle
  7842. this.beginPath();
  7843. this.moveTo(x, ym);
  7844. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7845. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7846. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7847. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7848. };
  7849. /**
  7850. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7851. */
  7852. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7853. var f = 1/3;
  7854. var wEllipse = w;
  7855. var hEllipse = h * f;
  7856. var kappa = .5522848,
  7857. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7858. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7859. xe = x + wEllipse, // x-end
  7860. ye = y + hEllipse, // y-end
  7861. xm = x + wEllipse / 2, // x-middle
  7862. ym = y + hEllipse / 2, // y-middle
  7863. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7864. yeb = y + h; // y-end, bottom ellipse
  7865. this.beginPath();
  7866. this.moveTo(xe, ym);
  7867. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7868. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7869. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7870. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7871. this.lineTo(xe, ymb);
  7872. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7873. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7874. this.lineTo(x, ym);
  7875. };
  7876. /**
  7877. * Draw an arrow point (no line)
  7878. */
  7879. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7880. // tail
  7881. var xt = x - length * Math.cos(angle);
  7882. var yt = y - length * Math.sin(angle);
  7883. // inner tail
  7884. // TODO: allow to customize different shapes
  7885. var xi = x - length * 0.9 * Math.cos(angle);
  7886. var yi = y - length * 0.9 * Math.sin(angle);
  7887. // left
  7888. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7889. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7890. // right
  7891. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7892. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7893. this.beginPath();
  7894. this.moveTo(x, y);
  7895. this.lineTo(xl, yl);
  7896. this.lineTo(xi, yi);
  7897. this.lineTo(xr, yr);
  7898. this.closePath();
  7899. };
  7900. /**
  7901. * Sets up the dashedLine functionality for drawing
  7902. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7903. * @author David Jordan
  7904. * @date 2012-08-08
  7905. */
  7906. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7907. if (!dashArray) dashArray=[10,5];
  7908. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7909. var dashCount = dashArray.length;
  7910. this.moveTo(x, y);
  7911. var dx = (x2-x), dy = (y2-y);
  7912. var slope = dy/dx;
  7913. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7914. var dashIndex=0, draw=true;
  7915. while (distRemaining>=0.1){
  7916. var dashLength = dashArray[dashIndex++%dashCount];
  7917. if (dashLength > distRemaining) dashLength = distRemaining;
  7918. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7919. if (dx<0) xStep = -xStep;
  7920. x += xStep;
  7921. y += slope*xStep;
  7922. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7923. distRemaining -= dashLength;
  7924. draw = !draw;
  7925. }
  7926. };
  7927. // TODO: add diamond shape
  7928. }
  7929. /**
  7930. * @class Node
  7931. * A node. A node can be connected to other nodes via one or multiple edges.
  7932. * @param {object} properties An object containing properties for the node. All
  7933. * properties are optional, except for the id.
  7934. * {number} id Id of the node. Required
  7935. * {string} label Text label for the node
  7936. * {number} x Horizontal position of the node
  7937. * {number} y Vertical position of the node
  7938. * {string} shape Node shape, available:
  7939. * "database", "circle", "ellipse",
  7940. * "box", "image", "text", "dot",
  7941. * "star", "triangle", "triangleDown",
  7942. * "square"
  7943. * {string} image An image url
  7944. * {string} title An title text, can be HTML
  7945. * {anytype} group A group name or number
  7946. * @param {Graph.Images} imagelist A list with images. Only needed
  7947. * when the node has an image
  7948. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7949. * retrieving group properties
  7950. * @param {Object} constants An object with default values for
  7951. * example for the color
  7952. *
  7953. */
  7954. function Node(properties, imagelist, grouplist, constants) {
  7955. this.selected = false;
  7956. this.hover = false;
  7957. this.edges = []; // all edges connected to this node
  7958. this.dynamicEdges = [];
  7959. this.reroutedEdges = {};
  7960. this.group = constants.nodes.group;
  7961. this.fontSize = Number(constants.nodes.fontSize);
  7962. this.fontFace = constants.nodes.fontFace;
  7963. this.fontColor = constants.nodes.fontColor;
  7964. this.fontDrawThreshold = 3;
  7965. this.color = constants.nodes.color;
  7966. // set defaults for the properties
  7967. this.id = undefined;
  7968. this.shape = constants.nodes.shape;
  7969. this.image = constants.nodes.image;
  7970. this.x = null;
  7971. this.y = null;
  7972. this.xFixed = false;
  7973. this.yFixed = false;
  7974. this.horizontalAlignLeft = true; // these are for the navigation controls
  7975. this.verticalAlignTop = true; // these are for the navigation controls
  7976. this.radius = constants.nodes.radius;
  7977. this.baseRadiusValue = constants.nodes.radius;
  7978. this.radiusFixed = false;
  7979. this.radiusMin = constants.nodes.radiusMin;
  7980. this.radiusMax = constants.nodes.radiusMax;
  7981. this.level = -1;
  7982. this.preassignedLevel = false;
  7983. this.imagelist = imagelist;
  7984. this.grouplist = grouplist;
  7985. // physics properties
  7986. this.fx = 0.0; // external force x
  7987. this.fy = 0.0; // external force y
  7988. this.vx = 0.0; // velocity x
  7989. this.vy = 0.0; // velocity y
  7990. this.minForce = constants.minForce;
  7991. this.damping = constants.physics.damping;
  7992. this.mass = 1; // kg
  7993. this.fixedData = {x:null,y:null};
  7994. this.setProperties(properties, constants);
  7995. // creating the variables for clustering
  7996. this.resetCluster();
  7997. this.dynamicEdgesLength = 0;
  7998. this.clusterSession = 0;
  7999. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  8000. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  8001. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  8002. this.maxNodeSizeIncrements = constants.clustering.maxNodeSizeIncrements;
  8003. this.growthIndicator = 0;
  8004. // variables to tell the node about the graph.
  8005. this.graphScaleInv = 1;
  8006. this.graphScale = 1;
  8007. this.canvasTopLeft = {"x": -300, "y": -300};
  8008. this.canvasBottomRight = {"x": 300, "y": 300};
  8009. this.parentEdgeId = null;
  8010. }
  8011. /**
  8012. * (re)setting the clustering variables and objects
  8013. */
  8014. Node.prototype.resetCluster = function() {
  8015. // clustering variables
  8016. this.formationScale = undefined; // this is used to determine when to open the cluster
  8017. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  8018. this.containedNodes = {};
  8019. this.containedEdges = {};
  8020. this.clusterSessions = [];
  8021. };
  8022. /**
  8023. * Attach a edge to the node
  8024. * @param {Edge} edge
  8025. */
  8026. Node.prototype.attachEdge = function(edge) {
  8027. if (this.edges.indexOf(edge) == -1) {
  8028. this.edges.push(edge);
  8029. }
  8030. if (this.dynamicEdges.indexOf(edge) == -1) {
  8031. this.dynamicEdges.push(edge);
  8032. }
  8033. this.dynamicEdgesLength = this.dynamicEdges.length;
  8034. };
  8035. /**
  8036. * Detach a edge from the node
  8037. * @param {Edge} edge
  8038. */
  8039. Node.prototype.detachEdge = function(edge) {
  8040. var index = this.edges.indexOf(edge);
  8041. if (index != -1) {
  8042. this.edges.splice(index, 1);
  8043. this.dynamicEdges.splice(index, 1);
  8044. }
  8045. this.dynamicEdgesLength = this.dynamicEdges.length;
  8046. };
  8047. /**
  8048. * Set or overwrite properties for the node
  8049. * @param {Object} properties an object with properties
  8050. * @param {Object} constants and object with default, global properties
  8051. */
  8052. Node.prototype.setProperties = function(properties, constants) {
  8053. if (!properties) {
  8054. return;
  8055. }
  8056. this.originalLabel = undefined;
  8057. // basic properties
  8058. if (properties.id !== undefined) {this.id = properties.id;}
  8059. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  8060. if (properties.title !== undefined) {this.title = properties.title;}
  8061. if (properties.group !== undefined) {this.group = properties.group;}
  8062. if (properties.x !== undefined) {this.x = properties.x;}
  8063. if (properties.y !== undefined) {this.y = properties.y;}
  8064. if (properties.value !== undefined) {this.value = properties.value;}
  8065. if (properties.level !== undefined) {this.level = properties.level; this.preassignedLevel = true;}
  8066. // physics
  8067. if (properties.mass !== undefined) {this.mass = properties.mass;}
  8068. // navigation controls properties
  8069. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  8070. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  8071. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  8072. if (this.id === undefined) {
  8073. throw "Node must have an id";
  8074. }
  8075. // copy group properties
  8076. if (this.group) {
  8077. var groupObj = this.grouplist.get(this.group);
  8078. for (var prop in groupObj) {
  8079. if (groupObj.hasOwnProperty(prop)) {
  8080. this[prop] = groupObj[prop];
  8081. }
  8082. }
  8083. }
  8084. // individual shape properties
  8085. if (properties.shape !== undefined) {this.shape = properties.shape;}
  8086. if (properties.image !== undefined) {this.image = properties.image;}
  8087. if (properties.radius !== undefined) {this.radius = properties.radius;}
  8088. if (properties.color !== undefined) {this.color = util.parseColor(properties.color);}
  8089. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8090. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8091. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8092. if (this.image !== undefined && this.image != "") {
  8093. if (this.imagelist) {
  8094. this.imageObj = this.imagelist.load(this.image);
  8095. }
  8096. else {
  8097. throw "No imagelist provided";
  8098. }
  8099. }
  8100. this.xFixed = this.xFixed || (properties.x !== undefined && !properties.allowedToMoveX);
  8101. this.yFixed = this.yFixed || (properties.y !== undefined && !properties.allowedToMoveY);
  8102. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  8103. if (this.shape == 'image') {
  8104. this.radiusMin = constants.nodes.widthMin;
  8105. this.radiusMax = constants.nodes.widthMax;
  8106. }
  8107. // choose draw method depending on the shape
  8108. switch (this.shape) {
  8109. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  8110. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  8111. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  8112. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8113. // TODO: add diamond shape
  8114. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  8115. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  8116. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  8117. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  8118. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  8119. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  8120. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  8121. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8122. }
  8123. // reset the size of the node, this can be changed
  8124. this._reset();
  8125. };
  8126. /**
  8127. * select this node
  8128. */
  8129. Node.prototype.select = function() {
  8130. this.selected = true;
  8131. this._reset();
  8132. };
  8133. /**
  8134. * unselect this node
  8135. */
  8136. Node.prototype.unselect = function() {
  8137. this.selected = false;
  8138. this._reset();
  8139. };
  8140. /**
  8141. * Reset the calculated size of the node, forces it to recalculate its size
  8142. */
  8143. Node.prototype.clearSizeCache = function() {
  8144. this._reset();
  8145. };
  8146. /**
  8147. * Reset the calculated size of the node, forces it to recalculate its size
  8148. * @private
  8149. */
  8150. Node.prototype._reset = function() {
  8151. this.width = undefined;
  8152. this.height = undefined;
  8153. };
  8154. /**
  8155. * get the title of this node.
  8156. * @return {string} title The title of the node, or undefined when no title
  8157. * has been set.
  8158. */
  8159. Node.prototype.getTitle = function() {
  8160. return typeof this.title === "function" ? this.title() : this.title;
  8161. };
  8162. /**
  8163. * Calculate the distance to the border of the Node
  8164. * @param {CanvasRenderingContext2D} ctx
  8165. * @param {Number} angle Angle in radians
  8166. * @returns {number} distance Distance to the border in pixels
  8167. */
  8168. Node.prototype.distanceToBorder = function (ctx, angle) {
  8169. var borderWidth = 1;
  8170. if (!this.width) {
  8171. this.resize(ctx);
  8172. }
  8173. switch (this.shape) {
  8174. case 'circle':
  8175. case 'dot':
  8176. return this.radius + borderWidth;
  8177. case 'ellipse':
  8178. var a = this.width / 2;
  8179. var b = this.height / 2;
  8180. var w = (Math.sin(angle) * a);
  8181. var h = (Math.cos(angle) * b);
  8182. return a * b / Math.sqrt(w * w + h * h);
  8183. // TODO: implement distanceToBorder for database
  8184. // TODO: implement distanceToBorder for triangle
  8185. // TODO: implement distanceToBorder for triangleDown
  8186. case 'box':
  8187. case 'image':
  8188. case 'text':
  8189. default:
  8190. if (this.width) {
  8191. return Math.min(
  8192. Math.abs(this.width / 2 / Math.cos(angle)),
  8193. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  8194. // TODO: reckon with border radius too in case of box
  8195. }
  8196. else {
  8197. return 0;
  8198. }
  8199. }
  8200. // TODO: implement calculation of distance to border for all shapes
  8201. };
  8202. /**
  8203. * Set forces acting on the node
  8204. * @param {number} fx Force in horizontal direction
  8205. * @param {number} fy Force in vertical direction
  8206. */
  8207. Node.prototype._setForce = function(fx, fy) {
  8208. this.fx = fx;
  8209. this.fy = fy;
  8210. };
  8211. /**
  8212. * Add forces acting on the node
  8213. * @param {number} fx Force in horizontal direction
  8214. * @param {number} fy Force in vertical direction
  8215. * @private
  8216. */
  8217. Node.prototype._addForce = function(fx, fy) {
  8218. this.fx += fx;
  8219. this.fy += fy;
  8220. };
  8221. /**
  8222. * Perform one discrete step for the node
  8223. * @param {number} interval Time interval in seconds
  8224. */
  8225. Node.prototype.discreteStep = function(interval) {
  8226. if (!this.xFixed) {
  8227. var dx = this.damping * this.vx; // damping force
  8228. var ax = (this.fx - dx) / this.mass; // acceleration
  8229. this.vx += ax * interval; // velocity
  8230. this.x += this.vx * interval; // position
  8231. }
  8232. if (!this.yFixed) {
  8233. var dy = this.damping * this.vy; // damping force
  8234. var ay = (this.fy - dy) / this.mass; // acceleration
  8235. this.vy += ay * interval; // velocity
  8236. this.y += this.vy * interval; // position
  8237. }
  8238. };
  8239. /**
  8240. * Perform one discrete step for the node
  8241. * @param {number} interval Time interval in seconds
  8242. * @param {number} maxVelocity The speed limit imposed on the velocity
  8243. */
  8244. Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
  8245. if (!this.xFixed) {
  8246. var dx = this.damping * this.vx; // damping force
  8247. var ax = (this.fx - dx) / this.mass; // acceleration
  8248. this.vx += ax * interval; // velocity
  8249. this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx;
  8250. this.x += this.vx * interval; // position
  8251. }
  8252. else {
  8253. this.fx = 0;
  8254. }
  8255. if (!this.yFixed) {
  8256. var dy = this.damping * this.vy; // damping force
  8257. var ay = (this.fy - dy) / this.mass; // acceleration
  8258. this.vy += ay * interval; // velocity
  8259. this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy;
  8260. this.y += this.vy * interval; // position
  8261. }
  8262. else {
  8263. this.fy = 0;
  8264. }
  8265. };
  8266. /**
  8267. * Check if this node has a fixed x and y position
  8268. * @return {boolean} true if fixed, false if not
  8269. */
  8270. Node.prototype.isFixed = function() {
  8271. return (this.xFixed && this.yFixed);
  8272. };
  8273. /**
  8274. * Check if this node is moving
  8275. * @param {number} vmin the minimum velocity considered as "moving"
  8276. * @return {boolean} true if moving, false if it has no velocity
  8277. */
  8278. // TODO: replace this method with calculating the kinetic energy
  8279. Node.prototype.isMoving = function(vmin) {
  8280. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  8281. };
  8282. /**
  8283. * check if this node is selecte
  8284. * @return {boolean} selected True if node is selected, else false
  8285. */
  8286. Node.prototype.isSelected = function() {
  8287. return this.selected;
  8288. };
  8289. /**
  8290. * Retrieve the value of the node. Can be undefined
  8291. * @return {Number} value
  8292. */
  8293. Node.prototype.getValue = function() {
  8294. return this.value;
  8295. };
  8296. /**
  8297. * Calculate the distance from the nodes location to the given location (x,y)
  8298. * @param {Number} x
  8299. * @param {Number} y
  8300. * @return {Number} value
  8301. */
  8302. Node.prototype.getDistance = function(x, y) {
  8303. var dx = this.x - x,
  8304. dy = this.y - y;
  8305. return Math.sqrt(dx * dx + dy * dy);
  8306. };
  8307. /**
  8308. * Adjust the value range of the node. The node will adjust it's radius
  8309. * based on its value.
  8310. * @param {Number} min
  8311. * @param {Number} max
  8312. */
  8313. Node.prototype.setValueRange = function(min, max) {
  8314. if (!this.radiusFixed && this.value !== undefined) {
  8315. if (max == min) {
  8316. this.radius = (this.radiusMin + this.radiusMax) / 2;
  8317. }
  8318. else {
  8319. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8320. this.radius = (this.value - min) * scale + this.radiusMin;
  8321. }
  8322. }
  8323. this.baseRadiusValue = this.radius;
  8324. };
  8325. /**
  8326. * Draw this node in the given canvas
  8327. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8328. * @param {CanvasRenderingContext2D} ctx
  8329. */
  8330. Node.prototype.draw = function(ctx) {
  8331. throw "Draw method not initialized for node";
  8332. };
  8333. /**
  8334. * Recalculate the size of this node in the given canvas
  8335. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8336. * @param {CanvasRenderingContext2D} ctx
  8337. */
  8338. Node.prototype.resize = function(ctx) {
  8339. throw "Resize method not initialized for node";
  8340. };
  8341. /**
  8342. * Check if this object is overlapping with the provided object
  8343. * @param {Object} obj an object with parameters left, top, right, bottom
  8344. * @return {boolean} True if location is located on node
  8345. */
  8346. Node.prototype.isOverlappingWith = function(obj) {
  8347. return (this.left < obj.right &&
  8348. this.left + this.width > obj.left &&
  8349. this.top < obj.bottom &&
  8350. this.top + this.height > obj.top);
  8351. };
  8352. Node.prototype._resizeImage = function (ctx) {
  8353. // TODO: pre calculate the image size
  8354. if (!this.width || !this.height) { // undefined or 0
  8355. var width, height;
  8356. if (this.value) {
  8357. this.radius = this.baseRadiusValue;
  8358. var scale = this.imageObj.height / this.imageObj.width;
  8359. if (scale !== undefined) {
  8360. width = this.radius || this.imageObj.width;
  8361. height = this.radius * scale || this.imageObj.height;
  8362. }
  8363. else {
  8364. width = 0;
  8365. height = 0;
  8366. }
  8367. }
  8368. else {
  8369. width = this.imageObj.width;
  8370. height = this.imageObj.height;
  8371. }
  8372. this.width = width;
  8373. this.height = height;
  8374. this.growthIndicator = 0;
  8375. if (this.width > 0 && this.height > 0) {
  8376. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8377. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8378. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8379. this.growthIndicator = this.width - width;
  8380. }
  8381. }
  8382. };
  8383. Node.prototype._drawImage = function (ctx) {
  8384. this._resizeImage(ctx);
  8385. this.left = this.x - this.width / 2;
  8386. this.top = this.y - this.height / 2;
  8387. var yLabel;
  8388. if (this.imageObj.width != 0 ) {
  8389. // draw the shade
  8390. if (this.clusterSize > 1) {
  8391. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  8392. lineWidth *= this.graphScaleInv;
  8393. lineWidth = Math.min(0.2 * this.width,lineWidth);
  8394. ctx.globalAlpha = 0.5;
  8395. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  8396. }
  8397. // draw the image
  8398. ctx.globalAlpha = 1.0;
  8399. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8400. yLabel = this.y + this.height / 2;
  8401. }
  8402. else {
  8403. // image still loading... just draw the label for now
  8404. yLabel = this.y;
  8405. }
  8406. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8407. };
  8408. Node.prototype._resizeBox = function (ctx) {
  8409. if (!this.width) {
  8410. var margin = 5;
  8411. var textSize = this.getTextSize(ctx);
  8412. this.width = textSize.width + 2 * margin;
  8413. this.height = textSize.height + 2 * margin;
  8414. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8415. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8416. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8417. // this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8418. }
  8419. };
  8420. Node.prototype._drawBox = function (ctx) {
  8421. this._resizeBox(ctx);
  8422. this.left = this.x - this.width / 2;
  8423. this.top = this.y - this.height / 2;
  8424. var clusterLineWidth = 2.5;
  8425. var selectionLineWidth = 2;
  8426. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  8427. // draw the outer border
  8428. if (this.clusterSize > 1) {
  8429. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8430. ctx.lineWidth *= this.graphScaleInv;
  8431. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8432. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  8433. ctx.stroke();
  8434. }
  8435. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8436. ctx.lineWidth *= this.graphScaleInv;
  8437. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8438. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8439. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8440. ctx.fill();
  8441. ctx.stroke();
  8442. this._label(ctx, this.label, this.x, this.y);
  8443. };
  8444. Node.prototype._resizeDatabase = function (ctx) {
  8445. if (!this.width) {
  8446. var margin = 5;
  8447. var textSize = this.getTextSize(ctx);
  8448. var size = textSize.width + 2 * margin;
  8449. this.width = size;
  8450. this.height = size;
  8451. // scaling used for clustering
  8452. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8453. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8454. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8455. this.growthIndicator = this.width - size;
  8456. }
  8457. };
  8458. Node.prototype._drawDatabase = function (ctx) {
  8459. this._resizeDatabase(ctx);
  8460. this.left = this.x - this.width / 2;
  8461. this.top = this.y - this.height / 2;
  8462. var clusterLineWidth = 2.5;
  8463. var selectionLineWidth = 2;
  8464. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  8465. // draw the outer border
  8466. if (this.clusterSize > 1) {
  8467. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8468. ctx.lineWidth *= this.graphScaleInv;
  8469. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8470. 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);
  8471. ctx.stroke();
  8472. }
  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.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  8477. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8478. ctx.fill();
  8479. ctx.stroke();
  8480. this._label(ctx, this.label, this.x, this.y);
  8481. };
  8482. Node.prototype._resizeCircle = function (ctx) {
  8483. if (!this.width) {
  8484. var margin = 5;
  8485. var textSize = this.getTextSize(ctx);
  8486. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8487. this.radius = diameter / 2;
  8488. this.width = diameter;
  8489. this.height = diameter;
  8490. // scaling used for clustering
  8491. // this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
  8492. // this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
  8493. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8494. this.growthIndicator = this.radius - 0.5*diameter;
  8495. }
  8496. };
  8497. Node.prototype._drawCircle = function (ctx) {
  8498. this._resizeCircle(ctx);
  8499. this.left = this.x - this.width / 2;
  8500. this.top = this.y - this.height / 2;
  8501. var clusterLineWidth = 2.5;
  8502. var selectionLineWidth = 2;
  8503. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  8504. // draw the outer border
  8505. if (this.clusterSize > 1) {
  8506. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8507. ctx.lineWidth *= this.graphScaleInv;
  8508. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8509. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  8510. ctx.stroke();
  8511. }
  8512. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8513. ctx.lineWidth *= this.graphScaleInv;
  8514. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8515. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  8516. ctx.circle(this.x, this.y, this.radius);
  8517. ctx.fill();
  8518. ctx.stroke();
  8519. this._label(ctx, this.label, this.x, this.y);
  8520. };
  8521. Node.prototype._resizeEllipse = function (ctx) {
  8522. if (!this.width) {
  8523. var textSize = this.getTextSize(ctx);
  8524. this.width = textSize.width * 1.5;
  8525. this.height = textSize.height * 2;
  8526. if (this.width < this.height) {
  8527. this.width = this.height;
  8528. }
  8529. var defaultSize = this.width;
  8530. // scaling used for clustering
  8531. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8532. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8533. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8534. this.growthIndicator = this.width - defaultSize;
  8535. }
  8536. };
  8537. Node.prototype._drawEllipse = function (ctx) {
  8538. this._resizeEllipse(ctx);
  8539. this.left = this.x - this.width / 2;
  8540. this.top = this.y - this.height / 2;
  8541. var clusterLineWidth = 2.5;
  8542. var selectionLineWidth = 2;
  8543. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  8544. // draw the outer border
  8545. if (this.clusterSize > 1) {
  8546. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8547. ctx.lineWidth *= this.graphScaleInv;
  8548. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8549. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  8550. ctx.stroke();
  8551. }
  8552. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8553. ctx.lineWidth *= this.graphScaleInv;
  8554. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8555. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  8556. ctx.ellipse(this.left, this.top, this.width, this.height);
  8557. ctx.fill();
  8558. ctx.stroke();
  8559. this._label(ctx, this.label, this.x, this.y);
  8560. };
  8561. Node.prototype._drawDot = function (ctx) {
  8562. this._drawShape(ctx, 'circle');
  8563. };
  8564. Node.prototype._drawTriangle = function (ctx) {
  8565. this._drawShape(ctx, 'triangle');
  8566. };
  8567. Node.prototype._drawTriangleDown = function (ctx) {
  8568. this._drawShape(ctx, 'triangleDown');
  8569. };
  8570. Node.prototype._drawSquare = function (ctx) {
  8571. this._drawShape(ctx, 'square');
  8572. };
  8573. Node.prototype._drawStar = function (ctx) {
  8574. this._drawShape(ctx, 'star');
  8575. };
  8576. Node.prototype._resizeShape = function (ctx) {
  8577. if (!this.width) {
  8578. this.radius = this.baseRadiusValue;
  8579. var size = 2 * this.radius;
  8580. this.width = size;
  8581. this.height = size;
  8582. // scaling used for clustering
  8583. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8584. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8585. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
  8586. this.growthIndicator = this.width - size;
  8587. }
  8588. };
  8589. Node.prototype._drawShape = function (ctx, shape) {
  8590. this._resizeShape(ctx);
  8591. this.left = this.x - this.width / 2;
  8592. this.top = this.y - this.height / 2;
  8593. var clusterLineWidth = 2.5;
  8594. var selectionLineWidth = 2;
  8595. var radiusMultiplier = 2;
  8596. // choose draw method depending on the shape
  8597. switch (shape) {
  8598. case 'dot': radiusMultiplier = 2; break;
  8599. case 'square': radiusMultiplier = 2; break;
  8600. case 'triangle': radiusMultiplier = 3; break;
  8601. case 'triangleDown': radiusMultiplier = 3; break;
  8602. case 'star': radiusMultiplier = 4; break;
  8603. }
  8604. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.hover ? this.color.hover.border : this.color.border;
  8605. // draw the outer border
  8606. if (this.clusterSize > 1) {
  8607. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8608. ctx.lineWidth *= this.graphScaleInv;
  8609. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8610. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  8611. ctx.stroke();
  8612. }
  8613. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8614. ctx.lineWidth *= this.graphScaleInv;
  8615. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8616. ctx.fillStyle = this.selected ? this.color.highlight.background : this.hover ? this.color.hover.background : this.color.background;
  8617. ctx[shape](this.x, this.y, this.radius);
  8618. ctx.fill();
  8619. ctx.stroke();
  8620. if (this.label) {
  8621. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8622. }
  8623. };
  8624. Node.prototype._resizeText = function (ctx) {
  8625. if (!this.width) {
  8626. var margin = 5;
  8627. var textSize = this.getTextSize(ctx);
  8628. this.width = textSize.width + 2 * margin;
  8629. this.height = textSize.height + 2 * margin;
  8630. // scaling used for clustering
  8631. this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
  8632. this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
  8633. this.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
  8634. this.growthIndicator = this.width - (textSize.width + 2 * margin);
  8635. }
  8636. };
  8637. Node.prototype._drawText = function (ctx) {
  8638. this._resizeText(ctx);
  8639. this.left = this.x - this.width / 2;
  8640. this.top = this.y - this.height / 2;
  8641. this._label(ctx, this.label, this.x, this.y);
  8642. };
  8643. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8644. if (text && this.fontSize * this.graphScale > this.fontDrawThreshold) {
  8645. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8646. ctx.fillStyle = this.fontColor || "black";
  8647. ctx.textAlign = align || "center";
  8648. ctx.textBaseline = baseline || "middle";
  8649. var lines = text.split('\n'),
  8650. lineCount = lines.length,
  8651. fontSize = (this.fontSize + 4),
  8652. yLine = y + (1 - lineCount) / 2 * fontSize;
  8653. for (var i = 0; i < lineCount; i++) {
  8654. ctx.fillText(lines[i], x, yLine);
  8655. yLine += fontSize;
  8656. }
  8657. }
  8658. };
  8659. Node.prototype.getTextSize = function(ctx) {
  8660. if (this.label !== undefined) {
  8661. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8662. var lines = this.label.split('\n'),
  8663. height = (this.fontSize + 4) * lines.length,
  8664. width = 0;
  8665. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8666. width = Math.max(width, ctx.measureText(lines[i]).width);
  8667. }
  8668. return {"width": width, "height": height};
  8669. }
  8670. else {
  8671. return {"width": 0, "height": 0};
  8672. }
  8673. };
  8674. /**
  8675. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  8676. * there is a safety margin of 0.3 * width;
  8677. *
  8678. * @returns {boolean}
  8679. */
  8680. Node.prototype.inArea = function() {
  8681. if (this.width !== undefined) {
  8682. return (this.x + this.width *this.graphScaleInv >= this.canvasTopLeft.x &&
  8683. this.x - this.width *this.graphScaleInv < this.canvasBottomRight.x &&
  8684. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  8685. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  8686. }
  8687. else {
  8688. return true;
  8689. }
  8690. };
  8691. /**
  8692. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  8693. * @returns {boolean}
  8694. */
  8695. Node.prototype.inView = function() {
  8696. return (this.x >= this.canvasTopLeft.x &&
  8697. this.x < this.canvasBottomRight.x &&
  8698. this.y >= this.canvasTopLeft.y &&
  8699. this.y < this.canvasBottomRight.y);
  8700. };
  8701. /**
  8702. * This allows the zoom level of the graph to influence the rendering
  8703. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  8704. *
  8705. * @param scale
  8706. * @param canvasTopLeft
  8707. * @param canvasBottomRight
  8708. */
  8709. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  8710. this.graphScaleInv = 1.0/scale;
  8711. this.graphScale = scale;
  8712. this.canvasTopLeft = canvasTopLeft;
  8713. this.canvasBottomRight = canvasBottomRight;
  8714. };
  8715. /**
  8716. * This allows the zoom level of the graph to influence the rendering
  8717. *
  8718. * @param scale
  8719. */
  8720. Node.prototype.setScale = function(scale) {
  8721. this.graphScaleInv = 1.0/scale;
  8722. this.graphScale = scale;
  8723. };
  8724. /**
  8725. * set the velocity at 0. Is called when this node is contained in another during clustering
  8726. */
  8727. Node.prototype.clearVelocity = function() {
  8728. this.vx = 0;
  8729. this.vy = 0;
  8730. };
  8731. /**
  8732. * Basic preservation of (kinectic) energy
  8733. *
  8734. * @param massBeforeClustering
  8735. */
  8736. Node.prototype.updateVelocity = function(massBeforeClustering) {
  8737. var energyBefore = this.vx * this.vx * massBeforeClustering;
  8738. //this.vx = (this.vx < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8739. this.vx = Math.sqrt(energyBefore/this.mass);
  8740. energyBefore = this.vy * this.vy * massBeforeClustering;
  8741. //this.vy = (this.vy < 0) ? -Math.sqrt(energyBefore/this.mass) : Math.sqrt(energyBefore/this.mass);
  8742. this.vy = Math.sqrt(energyBefore/this.mass);
  8743. };
  8744. /**
  8745. * @class Edge
  8746. *
  8747. * A edge connects two nodes
  8748. * @param {Object} properties Object with properties. Must contain
  8749. * At least properties from and to.
  8750. * Available properties: from (number),
  8751. * to (number), label (string, color (string),
  8752. * width (number), style (string),
  8753. * length (number), title (string)
  8754. * @param {Graph} graph A graph object, used to find and edge to
  8755. * nodes.
  8756. * @param {Object} constants An object with default values for
  8757. * example for the color
  8758. */
  8759. function Edge (properties, graph, constants) {
  8760. if (!graph) {
  8761. throw "No graph provided";
  8762. }
  8763. this.graph = graph;
  8764. // initialize constants
  8765. this.widthMin = constants.edges.widthMin;
  8766. this.widthMax = constants.edges.widthMax;
  8767. // initialize variables
  8768. this.id = undefined;
  8769. this.fromId = undefined;
  8770. this.toId = undefined;
  8771. this.style = constants.edges.style;
  8772. this.title = undefined;
  8773. this.width = constants.edges.width;
  8774. this.hoverWidth = constants.edges.hoverWidth;
  8775. this.value = undefined;
  8776. this.length = constants.physics.springLength;
  8777. this.customLength = false;
  8778. this.selected = false;
  8779. this.hover = false;
  8780. this.smooth = constants.smoothCurves;
  8781. this.arrowScaleFactor = constants.edges.arrowScaleFactor;
  8782. this.from = null; // a node
  8783. this.to = null; // a node
  8784. this.via = null; // a temp node
  8785. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  8786. // by storing the original information we can revert to the original connection when the cluser is opened.
  8787. this.originalFromId = [];
  8788. this.originalToId = [];
  8789. this.connected = false;
  8790. // Added to support dashed lines
  8791. // David Jordan
  8792. // 2012-08-08
  8793. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8794. this.color = {color:constants.edges.color.color,
  8795. highlight:constants.edges.color.highlight,
  8796. hover:constants.edges.color.hover};
  8797. this.widthFixed = false;
  8798. this.lengthFixed = false;
  8799. this.setProperties(properties, constants);
  8800. }
  8801. /**
  8802. * Set or overwrite properties for the edge
  8803. * @param {Object} properties an object with properties
  8804. * @param {Object} constants and object with default, global properties
  8805. */
  8806. Edge.prototype.setProperties = function(properties, constants) {
  8807. if (!properties) {
  8808. return;
  8809. }
  8810. if (properties.from !== undefined) {this.fromId = properties.from;}
  8811. if (properties.to !== undefined) {this.toId = properties.to;}
  8812. if (properties.id !== undefined) {this.id = properties.id;}
  8813. if (properties.style !== undefined) {this.style = properties.style;}
  8814. if (properties.label !== undefined) {this.label = properties.label;}
  8815. if (this.label) {
  8816. this.fontSize = constants.edges.fontSize;
  8817. this.fontFace = constants.edges.fontFace;
  8818. this.fontColor = constants.edges.fontColor;
  8819. this.fontFill = constants.edges.fontFill;
  8820. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8821. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8822. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8823. if (properties.fontFill !== undefined) {this.fontFill = properties.fontFill;}
  8824. }
  8825. if (properties.title !== undefined) {this.title = properties.title;}
  8826. if (properties.width !== undefined) {this.width = properties.width;}
  8827. if (properties.hoverWidth !== undefined) {this.hoverWidth = properties.hoverWidth;}
  8828. if (properties.value !== undefined) {this.value = properties.value;}
  8829. if (properties.length !== undefined) {this.length = properties.length;
  8830. this.customLength = true;}
  8831. // scale the arrow
  8832. if (properties.arrowScaleFactor !== undefined) {this.arrowScaleFactor = properties.arrowScaleFactor;}
  8833. // Added to support dashed lines
  8834. // David Jordan
  8835. // 2012-08-08
  8836. if (properties.dash) {
  8837. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  8838. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  8839. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  8840. }
  8841. if (properties.color !== undefined) {
  8842. if (util.isString(properties.color)) {
  8843. this.color.color = properties.color;
  8844. this.color.highlight = properties.color;
  8845. }
  8846. else {
  8847. if (properties.color.color !== undefined) {this.color.color = properties.color.color;}
  8848. if (properties.color.highlight !== undefined) {this.color.highlight = properties.color.highlight;}
  8849. }
  8850. }
  8851. // A node is connected when it has a from and to node.
  8852. this.connect();
  8853. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  8854. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  8855. // set draw method based on style
  8856. switch (this.style) {
  8857. case 'line': this.draw = this._drawLine; break;
  8858. case 'arrow': this.draw = this._drawArrow; break;
  8859. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8860. case 'dash-line': this.draw = this._drawDashLine; break;
  8861. default: this.draw = this._drawLine; break;
  8862. }
  8863. };
  8864. /**
  8865. * Connect an edge to its nodes
  8866. */
  8867. Edge.prototype.connect = function () {
  8868. this.disconnect();
  8869. this.from = this.graph.nodes[this.fromId] || null;
  8870. this.to = this.graph.nodes[this.toId] || null;
  8871. this.connected = (this.from && this.to);
  8872. if (this.connected) {
  8873. this.from.attachEdge(this);
  8874. this.to.attachEdge(this);
  8875. }
  8876. else {
  8877. if (this.from) {
  8878. this.from.detachEdge(this);
  8879. }
  8880. if (this.to) {
  8881. this.to.detachEdge(this);
  8882. }
  8883. }
  8884. };
  8885. /**
  8886. * Disconnect an edge from its nodes
  8887. */
  8888. Edge.prototype.disconnect = function () {
  8889. if (this.from) {
  8890. this.from.detachEdge(this);
  8891. this.from = null;
  8892. }
  8893. if (this.to) {
  8894. this.to.detachEdge(this);
  8895. this.to = null;
  8896. }
  8897. this.connected = false;
  8898. };
  8899. /**
  8900. * get the title of this edge.
  8901. * @return {string} title The title of the edge, or undefined when no title
  8902. * has been set.
  8903. */
  8904. Edge.prototype.getTitle = function() {
  8905. return typeof this.title === "function" ? this.title() : this.title;
  8906. };
  8907. /**
  8908. * Retrieve the value of the edge. Can be undefined
  8909. * @return {Number} value
  8910. */
  8911. Edge.prototype.getValue = function() {
  8912. return this.value;
  8913. };
  8914. /**
  8915. * Adjust the value range of the edge. The edge will adjust it's width
  8916. * based on its value.
  8917. * @param {Number} min
  8918. * @param {Number} max
  8919. */
  8920. Edge.prototype.setValueRange = function(min, max) {
  8921. if (!this.widthFixed && this.value !== undefined) {
  8922. var scale = (this.widthMax - this.widthMin) / (max - min);
  8923. this.width = (this.value - min) * scale + this.widthMin;
  8924. }
  8925. };
  8926. /**
  8927. * Redraw a edge
  8928. * Draw this edge in the given canvas
  8929. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8930. * @param {CanvasRenderingContext2D} ctx
  8931. */
  8932. Edge.prototype.draw = function(ctx) {
  8933. throw "Method draw not initialized in edge";
  8934. };
  8935. /**
  8936. * Check if this object is overlapping with the provided object
  8937. * @param {Object} obj an object with parameters left, top
  8938. * @return {boolean} True if location is located on the edge
  8939. */
  8940. Edge.prototype.isOverlappingWith = function(obj) {
  8941. if (this.connected) {
  8942. var distMax = 10;
  8943. var xFrom = this.from.x;
  8944. var yFrom = this.from.y;
  8945. var xTo = this.to.x;
  8946. var yTo = this.to.y;
  8947. var xObj = obj.left;
  8948. var yObj = obj.top;
  8949. var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8950. return (dist < distMax);
  8951. }
  8952. else {
  8953. return false
  8954. }
  8955. };
  8956. /**
  8957. * Redraw a edge as a line
  8958. * Draw this edge in the given canvas
  8959. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8960. * @param {CanvasRenderingContext2D} ctx
  8961. * @private
  8962. */
  8963. Edge.prototype._drawLine = function(ctx) {
  8964. // set style
  8965. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  8966. else if (this.hover == true) {ctx.strokeStyle = this.color.hover;}
  8967. else {ctx.strokeStyle = this.color.color;}
  8968. ctx.lineWidth = this._getLineWidth();
  8969. if (this.from != this.to) {
  8970. // draw line
  8971. this._line(ctx);
  8972. // draw label
  8973. var point;
  8974. if (this.label) {
  8975. if (this.smooth == true) {
  8976. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  8977. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  8978. point = {x:midpointX, y:midpointY};
  8979. }
  8980. else {
  8981. point = this._pointOnLine(0.5);
  8982. }
  8983. this._label(ctx, this.label, point.x, point.y);
  8984. }
  8985. }
  8986. else {
  8987. var x, y;
  8988. var radius = this.length / 4;
  8989. var node = this.from;
  8990. if (!node.width) {
  8991. node.resize(ctx);
  8992. }
  8993. if (node.width > node.height) {
  8994. x = node.x + node.width / 2;
  8995. y = node.y - radius;
  8996. }
  8997. else {
  8998. x = node.x + radius;
  8999. y = node.y - node.height / 2;
  9000. }
  9001. this._circle(ctx, x, y, radius);
  9002. point = this._pointOnCircle(x, y, radius, 0.5);
  9003. this._label(ctx, this.label, point.x, point.y);
  9004. }
  9005. };
  9006. /**
  9007. * Get the line width of the edge. Depends on width and whether one of the
  9008. * connected nodes is selected.
  9009. * @return {Number} width
  9010. * @private
  9011. */
  9012. Edge.prototype._getLineWidth = function() {
  9013. if (this.selected == true) {
  9014. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  9015. }
  9016. else {
  9017. if (this.hover == true) {
  9018. return Math.min(this.hoverWidth, this.widthMax)*this.graphScaleInv;
  9019. }
  9020. else {
  9021. return this.width*this.graphScaleInv;
  9022. }
  9023. }
  9024. };
  9025. /**
  9026. * Draw a line between two nodes
  9027. * @param {CanvasRenderingContext2D} ctx
  9028. * @private
  9029. */
  9030. Edge.prototype._line = function (ctx) {
  9031. // draw a straight line
  9032. ctx.beginPath();
  9033. ctx.moveTo(this.from.x, this.from.y);
  9034. if (this.smooth == true) {
  9035. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9036. }
  9037. else {
  9038. ctx.lineTo(this.to.x, this.to.y);
  9039. }
  9040. ctx.stroke();
  9041. };
  9042. /**
  9043. * Draw a line from a node to itself, a circle
  9044. * @param {CanvasRenderingContext2D} ctx
  9045. * @param {Number} x
  9046. * @param {Number} y
  9047. * @param {Number} radius
  9048. * @private
  9049. */
  9050. Edge.prototype._circle = function (ctx, x, y, radius) {
  9051. // draw a circle
  9052. ctx.beginPath();
  9053. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9054. ctx.stroke();
  9055. };
  9056. /**
  9057. * Draw label with white background and with the middle at (x, y)
  9058. * @param {CanvasRenderingContext2D} ctx
  9059. * @param {String} text
  9060. * @param {Number} x
  9061. * @param {Number} y
  9062. * @private
  9063. */
  9064. Edge.prototype._label = function (ctx, text, x, y) {
  9065. if (text) {
  9066. // TODO: cache the calculated size
  9067. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  9068. this.fontSize + "px " + this.fontFace;
  9069. ctx.fillStyle = this.fontFill;
  9070. var width = ctx.measureText(text).width;
  9071. var height = this.fontSize;
  9072. var left = x - width / 2;
  9073. var top = y - height / 2;
  9074. ctx.fillRect(left, top, width, height);
  9075. // draw text
  9076. ctx.fillStyle = this.fontColor || "black";
  9077. ctx.textAlign = "left";
  9078. ctx.textBaseline = "top";
  9079. ctx.fillText(text, left, top);
  9080. }
  9081. };
  9082. /**
  9083. * Redraw a edge as a dashed line
  9084. * Draw this edge in the given canvas
  9085. * @author David Jordan
  9086. * @date 2012-08-08
  9087. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9088. * @param {CanvasRenderingContext2D} ctx
  9089. * @private
  9090. */
  9091. Edge.prototype._drawDashLine = function(ctx) {
  9092. // set style
  9093. if (this.selected == true) {ctx.strokeStyle = this.color.highlight;}
  9094. else if (this.hover == true) {ctx.strokeStyle = this.color.hover;}
  9095. else {ctx.strokeStyle = this.color.color;}
  9096. ctx.lineWidth = this._getLineWidth();
  9097. // only firefox and chrome support this method, else we use the legacy one.
  9098. if (ctx.mozDash !== undefined || ctx.setLineDash !== undefined) {
  9099. ctx.beginPath();
  9100. ctx.moveTo(this.from.x, this.from.y);
  9101. // configure the dash pattern
  9102. var pattern = [0];
  9103. if (this.dash.length !== undefined && this.dash.gap !== undefined) {
  9104. pattern = [this.dash.length,this.dash.gap];
  9105. }
  9106. else {
  9107. pattern = [5,5];
  9108. }
  9109. // set dash settings for chrome or firefox
  9110. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9111. ctx.setLineDash(pattern);
  9112. ctx.lineDashOffset = 0;
  9113. } else { //Firefox
  9114. ctx.mozDash = pattern;
  9115. ctx.mozDashOffset = 0;
  9116. }
  9117. // draw the line
  9118. if (this.smooth == true) {
  9119. ctx.quadraticCurveTo(this.via.x,this.via.y,this.to.x, this.to.y);
  9120. }
  9121. else {
  9122. ctx.lineTo(this.to.x, this.to.y);
  9123. }
  9124. ctx.stroke();
  9125. // restore the dash settings.
  9126. if (typeof ctx.setLineDash !== 'undefined') { //Chrome
  9127. ctx.setLineDash([0]);
  9128. ctx.lineDashOffset = 0;
  9129. } else { //Firefox
  9130. ctx.mozDash = [0];
  9131. ctx.mozDashOffset = 0;
  9132. }
  9133. }
  9134. else { // unsupporting smooth lines
  9135. // draw dashed line
  9136. ctx.beginPath();
  9137. ctx.lineCap = 'round';
  9138. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  9139. {
  9140. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9141. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  9142. }
  9143. 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
  9144. {
  9145. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9146. [this.dash.length,this.dash.gap]);
  9147. }
  9148. else //If all else fails draw a line
  9149. {
  9150. ctx.moveTo(this.from.x, this.from.y);
  9151. ctx.lineTo(this.to.x, this.to.y);
  9152. }
  9153. ctx.stroke();
  9154. }
  9155. // draw label
  9156. if (this.label) {
  9157. var point;
  9158. if (this.smooth == true) {
  9159. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9160. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9161. point = {x:midpointX, y:midpointY};
  9162. }
  9163. else {
  9164. point = this._pointOnLine(0.5);
  9165. }
  9166. this._label(ctx, this.label, point.x, point.y);
  9167. }
  9168. };
  9169. /**
  9170. * Get a point on a line
  9171. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9172. * @return {Object} point
  9173. * @private
  9174. */
  9175. Edge.prototype._pointOnLine = function (percentage) {
  9176. return {
  9177. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  9178. y: (1 - percentage) * this.from.y + percentage * this.to.y
  9179. }
  9180. };
  9181. /**
  9182. * Get a point on a circle
  9183. * @param {Number} x
  9184. * @param {Number} y
  9185. * @param {Number} radius
  9186. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9187. * @return {Object} point
  9188. * @private
  9189. */
  9190. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  9191. var angle = (percentage - 3/8) * 2 * Math.PI;
  9192. return {
  9193. x: x + radius * Math.cos(angle),
  9194. y: y - radius * Math.sin(angle)
  9195. }
  9196. };
  9197. /**
  9198. * Redraw a edge as a line with an arrow halfway the line
  9199. * Draw this edge in the given canvas
  9200. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9201. * @param {CanvasRenderingContext2D} ctx
  9202. * @private
  9203. */
  9204. Edge.prototype._drawArrowCenter = function(ctx) {
  9205. var point;
  9206. // set style
  9207. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9208. else if (this.hover == true) {ctx.strokeStyle = this.color.hover; ctx.fillStyle = this.color.hover;}
  9209. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9210. ctx.lineWidth = this._getLineWidth();
  9211. if (this.from != this.to) {
  9212. // draw line
  9213. this._line(ctx);
  9214. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9215. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9216. // draw an arrow halfway the line
  9217. if (this.smooth == true) {
  9218. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9219. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9220. point = {x:midpointX, y:midpointY};
  9221. }
  9222. else {
  9223. point = this._pointOnLine(0.5);
  9224. }
  9225. ctx.arrow(point.x, point.y, angle, length);
  9226. ctx.fill();
  9227. ctx.stroke();
  9228. // draw label
  9229. if (this.label) {
  9230. this._label(ctx, this.label, point.x, point.y);
  9231. }
  9232. }
  9233. else {
  9234. // draw circle
  9235. var x, y;
  9236. var radius = 0.25 * Math.max(100,this.length);
  9237. var node = this.from;
  9238. if (!node.width) {
  9239. node.resize(ctx);
  9240. }
  9241. if (node.width > node.height) {
  9242. x = node.x + node.width * 0.5;
  9243. y = node.y - radius;
  9244. }
  9245. else {
  9246. x = node.x + radius;
  9247. y = node.y - node.height * 0.5;
  9248. }
  9249. this._circle(ctx, x, y, radius);
  9250. // draw all arrows
  9251. var angle = 0.2 * Math.PI;
  9252. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9253. point = this._pointOnCircle(x, y, radius, 0.5);
  9254. ctx.arrow(point.x, point.y, angle, length);
  9255. ctx.fill();
  9256. ctx.stroke();
  9257. // draw label
  9258. if (this.label) {
  9259. point = this._pointOnCircle(x, y, radius, 0.5);
  9260. this._label(ctx, this.label, point.x, point.y);
  9261. }
  9262. }
  9263. };
  9264. /**
  9265. * Redraw a edge as a line with an arrow
  9266. * Draw this edge in the given canvas
  9267. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9268. * @param {CanvasRenderingContext2D} ctx
  9269. * @private
  9270. */
  9271. Edge.prototype._drawArrow = function(ctx) {
  9272. // set style
  9273. if (this.selected == true) {ctx.strokeStyle = this.color.highlight; ctx.fillStyle = this.color.highlight;}
  9274. else if (this.hover == true) {ctx.strokeStyle = this.color.hover; ctx.fillStyle = this.color.hover;}
  9275. else {ctx.strokeStyle = this.color.color; ctx.fillStyle = this.color.color;}
  9276. ctx.lineWidth = this._getLineWidth();
  9277. var angle, length;
  9278. //draw a line
  9279. if (this.from != this.to) {
  9280. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9281. var dx = (this.to.x - this.from.x);
  9282. var dy = (this.to.y - this.from.y);
  9283. var edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9284. var fromBorderDist = this.from.distanceToBorder(ctx, angle + Math.PI);
  9285. var fromBorderPoint = (edgeSegmentLength - fromBorderDist) / edgeSegmentLength;
  9286. var xFrom = (fromBorderPoint) * this.from.x + (1 - fromBorderPoint) * this.to.x;
  9287. var yFrom = (fromBorderPoint) * this.from.y + (1 - fromBorderPoint) * this.to.y;
  9288. if (this.smooth == true) {
  9289. angle = Math.atan2((this.to.y - this.via.y), (this.to.x - this.via.x));
  9290. dx = (this.to.x - this.via.x);
  9291. dy = (this.to.y - this.via.y);
  9292. edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
  9293. }
  9294. var toBorderDist = this.to.distanceToBorder(ctx, angle);
  9295. var toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
  9296. var xTo,yTo;
  9297. if (this.smooth == true) {
  9298. xTo = (1 - toBorderPoint) * this.via.x + toBorderPoint * this.to.x;
  9299. yTo = (1 - toBorderPoint) * this.via.y + toBorderPoint * this.to.y;
  9300. }
  9301. else {
  9302. xTo = (1 - toBorderPoint) * this.from.x + toBorderPoint * this.to.x;
  9303. yTo = (1 - toBorderPoint) * this.from.y + toBorderPoint * this.to.y;
  9304. }
  9305. ctx.beginPath();
  9306. ctx.moveTo(xFrom,yFrom);
  9307. if (this.smooth == true) {
  9308. ctx.quadraticCurveTo(this.via.x,this.via.y,xTo, yTo);
  9309. }
  9310. else {
  9311. ctx.lineTo(xTo, yTo);
  9312. }
  9313. ctx.stroke();
  9314. // draw arrow at the end of the line
  9315. length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9316. ctx.arrow(xTo, yTo, angle, length);
  9317. ctx.fill();
  9318. ctx.stroke();
  9319. // draw label
  9320. if (this.label) {
  9321. var point;
  9322. if (this.smooth == true) {
  9323. var midpointX = 0.5*(0.5*(this.from.x + this.via.x) + 0.5*(this.to.x + this.via.x));
  9324. var midpointY = 0.5*(0.5*(this.from.y + this.via.y) + 0.5*(this.to.y + this.via.y));
  9325. point = {x:midpointX, y:midpointY};
  9326. }
  9327. else {
  9328. point = this._pointOnLine(0.5);
  9329. }
  9330. this._label(ctx, this.label, point.x, point.y);
  9331. }
  9332. }
  9333. else {
  9334. // draw circle
  9335. var node = this.from;
  9336. var x, y, arrow;
  9337. var radius = 0.25 * Math.max(100,this.length);
  9338. if (!node.width) {
  9339. node.resize(ctx);
  9340. }
  9341. if (node.width > node.height) {
  9342. x = node.x + node.width * 0.5;
  9343. y = node.y - radius;
  9344. arrow = {
  9345. x: x,
  9346. y: node.y,
  9347. angle: 0.9 * Math.PI
  9348. };
  9349. }
  9350. else {
  9351. x = node.x + radius;
  9352. y = node.y - node.height * 0.5;
  9353. arrow = {
  9354. x: node.x,
  9355. y: y,
  9356. angle: 0.6 * Math.PI
  9357. };
  9358. }
  9359. ctx.beginPath();
  9360. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9361. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9362. ctx.stroke();
  9363. // draw all arrows
  9364. var length = (10 + 5 * this.width) * this.arrowScaleFactor;
  9365. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9366. ctx.fill();
  9367. ctx.stroke();
  9368. // draw label
  9369. if (this.label) {
  9370. point = this._pointOnCircle(x, y, radius, 0.5);
  9371. this._label(ctx, this.label, point.x, point.y);
  9372. }
  9373. }
  9374. };
  9375. /**
  9376. * Calculate the distance between a point (x3,y3) and a line segment from
  9377. * (x1,y1) to (x2,y2).
  9378. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9379. * @param {number} x1
  9380. * @param {number} y1
  9381. * @param {number} x2
  9382. * @param {number} y2
  9383. * @param {number} x3
  9384. * @param {number} y3
  9385. * @private
  9386. */
  9387. Edge.prototype._getDistanceToEdge = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9388. if (this.smooth == true) {
  9389. var minDistance = 1e9;
  9390. var i,t,x,y,dx,dy;
  9391. for (i = 0; i < 10; i++) {
  9392. t = 0.1*i;
  9393. x = Math.pow(1-t,2)*x1 + (2*t*(1 - t))*this.via.x + Math.pow(t,2)*x2;
  9394. y = Math.pow(1-t,2)*y1 + (2*t*(1 - t))*this.via.y + Math.pow(t,2)*y2;
  9395. dx = Math.abs(x3-x);
  9396. dy = Math.abs(y3-y);
  9397. minDistance = Math.min(minDistance,Math.sqrt(dx*dx + dy*dy));
  9398. }
  9399. return minDistance
  9400. }
  9401. else {
  9402. var px = x2-x1,
  9403. py = y2-y1,
  9404. something = px*px + py*py,
  9405. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9406. if (u > 1) {
  9407. u = 1;
  9408. }
  9409. else if (u < 0) {
  9410. u = 0;
  9411. }
  9412. var x = x1 + u * px,
  9413. y = y1 + u * py,
  9414. dx = x - x3,
  9415. dy = y - y3;
  9416. //# Note: If the actual distance does not matter,
  9417. //# if you only want to compare what this function
  9418. //# returns to other results of this function, you
  9419. //# can just return the squared distance instead
  9420. //# (i.e. remove the sqrt) to gain a little performance
  9421. return Math.sqrt(dx*dx + dy*dy);
  9422. }
  9423. };
  9424. /**
  9425. * This allows the zoom level of the graph to influence the rendering
  9426. *
  9427. * @param scale
  9428. */
  9429. Edge.prototype.setScale = function(scale) {
  9430. this.graphScaleInv = 1.0/scale;
  9431. };
  9432. Edge.prototype.select = function() {
  9433. this.selected = true;
  9434. };
  9435. Edge.prototype.unselect = function() {
  9436. this.selected = false;
  9437. };
  9438. Edge.prototype.positionBezierNode = function() {
  9439. if (this.via !== null) {
  9440. this.via.x = 0.5 * (this.from.x + this.to.x);
  9441. this.via.y = 0.5 * (this.from.y + this.to.y);
  9442. }
  9443. };
  9444. /**
  9445. * Popup is a class to create a popup window with some text
  9446. * @param {Element} container The container object.
  9447. * @param {Number} [x]
  9448. * @param {Number} [y]
  9449. * @param {String} [text]
  9450. * @param {Object} [style] An object containing borderColor,
  9451. * backgroundColor, etc.
  9452. */
  9453. function Popup(container, x, y, text, style) {
  9454. if (container) {
  9455. this.container = container;
  9456. }
  9457. else {
  9458. this.container = document.body;
  9459. }
  9460. // x, y and text are optional, see if a style object was passed in their place
  9461. if (style === undefined) {
  9462. if (typeof x === "object") {
  9463. style = x;
  9464. x = undefined;
  9465. } else if (typeof text === "object") {
  9466. style = text;
  9467. text = undefined;
  9468. } else {
  9469. // for backwards compatibility, in case clients other than Graph are creating Popup directly
  9470. style = {
  9471. fontColor: 'black',
  9472. fontSize: 14, // px
  9473. fontFace: 'verdana',
  9474. color: {
  9475. border: '#666',
  9476. background: '#FFFFC6'
  9477. }
  9478. }
  9479. }
  9480. }
  9481. this.x = 0;
  9482. this.y = 0;
  9483. this.padding = 5;
  9484. if (x !== undefined && y !== undefined ) {
  9485. this.setPosition(x, y);
  9486. }
  9487. if (text !== undefined) {
  9488. this.setText(text);
  9489. }
  9490. // create the frame
  9491. this.frame = document.createElement("div");
  9492. var styleAttr = this.frame.style;
  9493. styleAttr.position = "absolute";
  9494. styleAttr.visibility = "hidden";
  9495. styleAttr.border = "1px solid " + style.color.border;
  9496. styleAttr.color = style.fontColor;
  9497. styleAttr.fontSize = style.fontSize + "px";
  9498. styleAttr.fontFamily = style.fontFace;
  9499. styleAttr.padding = this.padding + "px";
  9500. styleAttr.backgroundColor = style.color.background;
  9501. styleAttr.borderRadius = "3px";
  9502. styleAttr.MozBorderRadius = "3px";
  9503. styleAttr.WebkitBorderRadius = "3px";
  9504. styleAttr.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  9505. styleAttr.whiteSpace = "nowrap";
  9506. this.container.appendChild(this.frame);
  9507. }
  9508. /**
  9509. * @param {number} x Horizontal position of the popup window
  9510. * @param {number} y Vertical position of the popup window
  9511. */
  9512. Popup.prototype.setPosition = function(x, y) {
  9513. this.x = parseInt(x);
  9514. this.y = parseInt(y);
  9515. };
  9516. /**
  9517. * Set the text for the popup window. This can be HTML code
  9518. * @param {string} text
  9519. */
  9520. Popup.prototype.setText = function(text) {
  9521. this.frame.innerHTML = text;
  9522. };
  9523. /**
  9524. * Show the popup window
  9525. * @param {boolean} show Optional. Show or hide the window
  9526. */
  9527. Popup.prototype.show = function (show) {
  9528. if (show === undefined) {
  9529. show = true;
  9530. }
  9531. if (show) {
  9532. var height = this.frame.clientHeight;
  9533. var width = this.frame.clientWidth;
  9534. var maxHeight = this.frame.parentNode.clientHeight;
  9535. var maxWidth = this.frame.parentNode.clientWidth;
  9536. var top = (this.y - height);
  9537. if (top + height + this.padding > maxHeight) {
  9538. top = maxHeight - height - this.padding;
  9539. }
  9540. if (top < this.padding) {
  9541. top = this.padding;
  9542. }
  9543. var left = this.x;
  9544. if (left + width + this.padding > maxWidth) {
  9545. left = maxWidth - width - this.padding;
  9546. }
  9547. if (left < this.padding) {
  9548. left = this.padding;
  9549. }
  9550. this.frame.style.left = left + "px";
  9551. this.frame.style.top = top + "px";
  9552. this.frame.style.visibility = "visible";
  9553. }
  9554. else {
  9555. this.hide();
  9556. }
  9557. };
  9558. /**
  9559. * Hide the popup window
  9560. */
  9561. Popup.prototype.hide = function () {
  9562. this.frame.style.visibility = "hidden";
  9563. };
  9564. /**
  9565. * @class Groups
  9566. * This class can store groups and properties specific for groups.
  9567. */
  9568. function Groups() {
  9569. this.clear();
  9570. this.defaultIndex = 0;
  9571. }
  9572. /**
  9573. * default constants for group colors
  9574. */
  9575. Groups.DEFAULT = [
  9576. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  9577. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  9578. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  9579. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  9580. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  9581. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  9582. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  9583. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  9584. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  9585. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  9586. ];
  9587. /**
  9588. * Clear all groups
  9589. */
  9590. Groups.prototype.clear = function () {
  9591. this.groups = {};
  9592. this.groups.length = function()
  9593. {
  9594. var i = 0;
  9595. for ( var p in this ) {
  9596. if (this.hasOwnProperty(p)) {
  9597. i++;
  9598. }
  9599. }
  9600. return i;
  9601. }
  9602. };
  9603. /**
  9604. * get group properties of a groupname. If groupname is not found, a new group
  9605. * is added.
  9606. * @param {*} groupname Can be a number, string, Date, etc.
  9607. * @return {Object} group The created group, containing all group properties
  9608. */
  9609. Groups.prototype.get = function (groupname) {
  9610. var group = this.groups[groupname];
  9611. if (group == undefined) {
  9612. // create new group
  9613. var index = this.defaultIndex % Groups.DEFAULT.length;
  9614. this.defaultIndex++;
  9615. group = {};
  9616. group.color = Groups.DEFAULT[index];
  9617. this.groups[groupname] = group;
  9618. }
  9619. return group;
  9620. };
  9621. /**
  9622. * Add a custom group style
  9623. * @param {String} groupname
  9624. * @param {Object} style An object containing borderColor,
  9625. * backgroundColor, etc.
  9626. * @return {Object} group The created group object
  9627. */
  9628. Groups.prototype.add = function (groupname, style) {
  9629. this.groups[groupname] = style;
  9630. if (style.color) {
  9631. style.color = util.parseColor(style.color);
  9632. }
  9633. return style;
  9634. };
  9635. /**
  9636. * @class Images
  9637. * This class loads images and keeps them stored.
  9638. */
  9639. function Images() {
  9640. this.images = {};
  9641. this.callback = undefined;
  9642. }
  9643. /**
  9644. * Set an onload callback function. This will be called each time an image
  9645. * is loaded
  9646. * @param {function} callback
  9647. */
  9648. Images.prototype.setOnloadCallback = function(callback) {
  9649. this.callback = callback;
  9650. };
  9651. /**
  9652. *
  9653. * @param {string} url Url of the image
  9654. * @return {Image} img The image object
  9655. */
  9656. Images.prototype.load = function(url) {
  9657. var img = this.images[url];
  9658. if (img == undefined) {
  9659. // create the image
  9660. var images = this;
  9661. img = new Image();
  9662. this.images[url] = img;
  9663. img.onload = function() {
  9664. if (images.callback) {
  9665. images.callback(this);
  9666. }
  9667. };
  9668. img.src = url;
  9669. }
  9670. return img;
  9671. };
  9672. /**
  9673. * Created by Alex on 2/6/14.
  9674. */
  9675. var physicsMixin = {
  9676. /**
  9677. * Toggling barnes Hut calculation on and off.
  9678. *
  9679. * @private
  9680. */
  9681. _toggleBarnesHut: function () {
  9682. this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled;
  9683. this._loadSelectedForceSolver();
  9684. this.moving = true;
  9685. this.start();
  9686. },
  9687. /**
  9688. * This loads the node force solver based on the barnes hut or repulsion algorithm
  9689. *
  9690. * @private
  9691. */
  9692. _loadSelectedForceSolver: function () {
  9693. // this overloads the this._calculateNodeForces
  9694. if (this.constants.physics.barnesHut.enabled == true) {
  9695. this._clearMixin(repulsionMixin);
  9696. this._clearMixin(hierarchalRepulsionMixin);
  9697. this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity;
  9698. this.constants.physics.springLength = this.constants.physics.barnesHut.springLength;
  9699. this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant;
  9700. this.constants.physics.damping = this.constants.physics.barnesHut.damping;
  9701. this._loadMixin(barnesHutMixin);
  9702. }
  9703. else if (this.constants.physics.hierarchicalRepulsion.enabled == true) {
  9704. this._clearMixin(barnesHutMixin);
  9705. this._clearMixin(repulsionMixin);
  9706. this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity;
  9707. this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength;
  9708. this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant;
  9709. this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping;
  9710. this._loadMixin(hierarchalRepulsionMixin);
  9711. }
  9712. else {
  9713. this._clearMixin(barnesHutMixin);
  9714. this._clearMixin(hierarchalRepulsionMixin);
  9715. this.barnesHutTree = undefined;
  9716. this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity;
  9717. this.constants.physics.springLength = this.constants.physics.repulsion.springLength;
  9718. this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant;
  9719. this.constants.physics.damping = this.constants.physics.repulsion.damping;
  9720. this._loadMixin(repulsionMixin);
  9721. }
  9722. },
  9723. /**
  9724. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  9725. * if there is more than one node. If it is just one node, we dont calculate anything.
  9726. *
  9727. * @private
  9728. */
  9729. _initializeForceCalculation: function () {
  9730. // stop calculation if there is only one node
  9731. if (this.nodeIndices.length == 1) {
  9732. this.nodes[this.nodeIndices[0]]._setForce(0, 0);
  9733. }
  9734. else {
  9735. // if there are too many nodes on screen, we cluster without repositioning
  9736. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  9737. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  9738. }
  9739. // we now start the force calculation
  9740. this._calculateForces();
  9741. }
  9742. },
  9743. /**
  9744. * Calculate the external forces acting on the nodes
  9745. * Forces are caused by: edges, repulsing forces between nodes, gravity
  9746. * @private
  9747. */
  9748. _calculateForces: function () {
  9749. // Gravity is required to keep separated groups from floating off
  9750. // the forces are reset to zero in this loop by using _setForce instead
  9751. // of _addForce
  9752. this._calculateGravitationalForces();
  9753. this._calculateNodeForces();
  9754. if (this.constants.smoothCurves == true) {
  9755. this._calculateSpringForcesWithSupport();
  9756. }
  9757. else {
  9758. this._calculateSpringForces();
  9759. }
  9760. },
  9761. /**
  9762. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  9763. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  9764. * This function joins the datanodes and invisible (called support) nodes into one object.
  9765. * We do this so we do not contaminate this.nodes with the support nodes.
  9766. *
  9767. * @private
  9768. */
  9769. _updateCalculationNodes: function () {
  9770. if (this.constants.smoothCurves == true) {
  9771. this.calculationNodes = {};
  9772. this.calculationNodeIndices = [];
  9773. for (var nodeId in this.nodes) {
  9774. if (this.nodes.hasOwnProperty(nodeId)) {
  9775. this.calculationNodes[nodeId] = this.nodes[nodeId];
  9776. }
  9777. }
  9778. var supportNodes = this.sectors['support']['nodes'];
  9779. for (var supportNodeId in supportNodes) {
  9780. if (supportNodes.hasOwnProperty(supportNodeId)) {
  9781. if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) {
  9782. this.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
  9783. }
  9784. else {
  9785. supportNodes[supportNodeId]._setForce(0, 0);
  9786. }
  9787. }
  9788. }
  9789. for (var idx in this.calculationNodes) {
  9790. if (this.calculationNodes.hasOwnProperty(idx)) {
  9791. this.calculationNodeIndices.push(idx);
  9792. }
  9793. }
  9794. }
  9795. else {
  9796. this.calculationNodes = this.nodes;
  9797. this.calculationNodeIndices = this.nodeIndices;
  9798. }
  9799. },
  9800. /**
  9801. * this function applies the central gravity effect to keep groups from floating off
  9802. *
  9803. * @private
  9804. */
  9805. _calculateGravitationalForces: function () {
  9806. var dx, dy, distance, node, i;
  9807. var nodes = this.calculationNodes;
  9808. var gravity = this.constants.physics.centralGravity;
  9809. var gravityForce = 0;
  9810. for (i = 0; i < this.calculationNodeIndices.length; i++) {
  9811. node = nodes[this.calculationNodeIndices[i]];
  9812. node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters.
  9813. // gravity does not apply when we are in a pocket sector
  9814. if (this._sector() == "default" && gravity != 0) {
  9815. dx = -node.x;
  9816. dy = -node.y;
  9817. distance = Math.sqrt(dx * dx + dy * dy);
  9818. gravityForce = (distance == 0) ? 0 : (gravity / distance);
  9819. node.fx = dx * gravityForce;
  9820. node.fy = dy * gravityForce;
  9821. }
  9822. else {
  9823. node.fx = 0;
  9824. node.fy = 0;
  9825. }
  9826. }
  9827. },
  9828. /**
  9829. * this function calculates the effects of the springs in the case of unsmooth curves.
  9830. *
  9831. * @private
  9832. */
  9833. _calculateSpringForces: function () {
  9834. var edgeLength, edge, edgeId;
  9835. var dx, dy, fx, fy, springForce, distance;
  9836. var edges = this.edges;
  9837. // forces caused by the edges, modelled as springs
  9838. for (edgeId in edges) {
  9839. if (edges.hasOwnProperty(edgeId)) {
  9840. edge = edges[edgeId];
  9841. if (edge.connected) {
  9842. // only calculate forces if nodes are in the same sector
  9843. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9844. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9845. // this implies that the edges between big clusters are longer
  9846. edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
  9847. dx = (edge.from.x - edge.to.x);
  9848. dy = (edge.from.y - edge.to.y);
  9849. distance = Math.sqrt(dx * dx + dy * dy);
  9850. if (distance == 0) {
  9851. distance = 0.01;
  9852. }
  9853. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  9854. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  9855. fx = dx * springForce;
  9856. fy = dy * springForce;
  9857. edge.from.fx += fx;
  9858. edge.from.fy += fy;
  9859. edge.to.fx -= fx;
  9860. edge.to.fy -= fy;
  9861. }
  9862. }
  9863. }
  9864. }
  9865. },
  9866. /**
  9867. * This function calculates the springforces on the nodes, accounting for the support nodes.
  9868. *
  9869. * @private
  9870. */
  9871. _calculateSpringForcesWithSupport: function () {
  9872. var edgeLength, edge, edgeId, combinedClusterSize;
  9873. var edges = this.edges;
  9874. // forces caused by the edges, modelled as springs
  9875. for (edgeId in edges) {
  9876. if (edges.hasOwnProperty(edgeId)) {
  9877. edge = edges[edgeId];
  9878. if (edge.connected) {
  9879. // only calculate forces if nodes are in the same sector
  9880. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  9881. if (edge.via != null) {
  9882. var node1 = edge.to;
  9883. var node2 = edge.via;
  9884. var node3 = edge.from;
  9885. edgeLength = edge.customLength ? edge.length : this.constants.physics.springLength;
  9886. combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
  9887. // this implies that the edges between big clusters are longer
  9888. edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
  9889. this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
  9890. this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
  9891. }
  9892. }
  9893. }
  9894. }
  9895. }
  9896. },
  9897. /**
  9898. * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
  9899. *
  9900. * @param node1
  9901. * @param node2
  9902. * @param edgeLength
  9903. * @private
  9904. */
  9905. _calculateSpringForce: function (node1, node2, edgeLength) {
  9906. var dx, dy, fx, fy, springForce, distance;
  9907. dx = (node1.x - node2.x);
  9908. dy = (node1.y - node2.y);
  9909. distance = Math.sqrt(dx * dx + dy * dy);
  9910. if (distance == 0) {
  9911. distance = 0.01;
  9912. }
  9913. // the 1/distance is so the fx and fy can be calculated without sine or cosine.
  9914. springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance;
  9915. fx = dx * springForce;
  9916. fy = dy * springForce;
  9917. node1.fx += fx;
  9918. node1.fy += fy;
  9919. node2.fx -= fx;
  9920. node2.fy -= fy;
  9921. },
  9922. /**
  9923. * Load the HTML for the physics config and bind it
  9924. * @private
  9925. */
  9926. _loadPhysicsConfiguration: function () {
  9927. if (this.physicsConfiguration === undefined) {
  9928. this.backupConstants = {};
  9929. util.copyObject(this.constants, this.backupConstants);
  9930. var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"];
  9931. this.physicsConfiguration = document.createElement('div');
  9932. this.physicsConfiguration.className = "PhysicsConfiguration";
  9933. this.physicsConfiguration.innerHTML = '' +
  9934. '<table><tr><td><b>Simulation Mode:</b></td></tr>' +
  9935. '<tr>' +
  9936. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' +
  9937. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' +
  9938. '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' +
  9939. '</tr>' +
  9940. '</table>' +
  9941. '<table id="graph_BH_table" style="display:none">' +
  9942. '<tr><td><b>Barnes Hut</b></td></tr>' +
  9943. '<tr>' +
  9944. '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="20000" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-20000</td><td><input value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' +
  9945. '</tr>' +
  9946. '<tr>' +
  9947. '<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>' +
  9948. '</tr>' +
  9949. '<tr>' +
  9950. '<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>' +
  9951. '</tr>' +
  9952. '<tr>' +
  9953. '<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>' +
  9954. '</tr>' +
  9955. '<tr>' +
  9956. '<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>' +
  9957. '</tr>' +
  9958. '</table>' +
  9959. '<table id="graph_R_table" style="display:none">' +
  9960. '<tr><td><b>Repulsion</b></td></tr>' +
  9961. '<tr>' +
  9962. '<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>' +
  9963. '</tr>' +
  9964. '<tr>' +
  9965. '<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>' +
  9966. '</tr>' +
  9967. '<tr>' +
  9968. '<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>' +
  9969. '</tr>' +
  9970. '<tr>' +
  9971. '<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>' +
  9972. '</tr>' +
  9973. '<tr>' +
  9974. '<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>' +
  9975. '</tr>' +
  9976. '</table>' +
  9977. '<table id="graph_H_table" style="display:none">' +
  9978. '<tr><td width="150"><b>Hierarchical</b></td></tr>' +
  9979. '<tr>' +
  9980. '<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>' +
  9981. '</tr>' +
  9982. '<tr>' +
  9983. '<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>' +
  9984. '</tr>' +
  9985. '<tr>' +
  9986. '<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>' +
  9987. '</tr>' +
  9988. '<tr>' +
  9989. '<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>' +
  9990. '</tr>' +
  9991. '<tr>' +
  9992. '<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>' +
  9993. '</tr>' +
  9994. '<tr>' +
  9995. '<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>' +
  9996. '</tr>' +
  9997. '<tr>' +
  9998. '<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>' +
  9999. '</tr>' +
  10000. '<tr>' +
  10001. '<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>' +
  10002. '</tr>' +
  10003. '</table>' +
  10004. '<table><tr><td><b>Options:</b></td></tr>' +
  10005. '<tr>' +
  10006. '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' +
  10007. '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' +
  10008. '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' +
  10009. '</tr>' +
  10010. '</table>'
  10011. this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement);
  10012. this.optionsDiv = document.createElement("div");
  10013. this.optionsDiv.style.fontSize = "14px";
  10014. this.optionsDiv.style.fontFamily = "verdana";
  10015. this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement);
  10016. var rangeElement;
  10017. rangeElement = document.getElementById('graph_BH_gc');
  10018. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant");
  10019. rangeElement = document.getElementById('graph_BH_cg');
  10020. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity");
  10021. rangeElement = document.getElementById('graph_BH_sc');
  10022. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant");
  10023. rangeElement = document.getElementById('graph_BH_sl');
  10024. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength");
  10025. rangeElement = document.getElementById('graph_BH_damp');
  10026. rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping");
  10027. rangeElement = document.getElementById('graph_R_nd');
  10028. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance");
  10029. rangeElement = document.getElementById('graph_R_cg');
  10030. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity");
  10031. rangeElement = document.getElementById('graph_R_sc');
  10032. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant");
  10033. rangeElement = document.getElementById('graph_R_sl');
  10034. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength");
  10035. rangeElement = document.getElementById('graph_R_damp');
  10036. rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping");
  10037. rangeElement = document.getElementById('graph_H_nd');
  10038. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance");
  10039. rangeElement = document.getElementById('graph_H_cg');
  10040. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity");
  10041. rangeElement = document.getElementById('graph_H_sc');
  10042. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant");
  10043. rangeElement = document.getElementById('graph_H_sl');
  10044. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength");
  10045. rangeElement = document.getElementById('graph_H_damp');
  10046. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping");
  10047. rangeElement = document.getElementById('graph_H_direction');
  10048. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction");
  10049. rangeElement = document.getElementById('graph_H_levsep');
  10050. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation");
  10051. rangeElement = document.getElementById('graph_H_nspac');
  10052. rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing");
  10053. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10054. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10055. var radioButton3 = document.getElementById("graph_physicsMethod3");
  10056. radioButton2.checked = true;
  10057. if (this.constants.physics.barnesHut.enabled) {
  10058. radioButton1.checked = true;
  10059. }
  10060. if (this.constants.hierarchicalLayout.enabled) {
  10061. radioButton3.checked = true;
  10062. }
  10063. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10064. var graph_repositionNodes = document.getElementById("graph_repositionNodes");
  10065. var graph_generateOptions = document.getElementById("graph_generateOptions");
  10066. graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this);
  10067. graph_repositionNodes.onclick = graphRepositionNodes.bind(this);
  10068. graph_generateOptions.onclick = graphGenerateOptions.bind(this);
  10069. if (this.constants.smoothCurves == true) {
  10070. graph_toggleSmooth.style.background = "#A4FF56";
  10071. }
  10072. else {
  10073. graph_toggleSmooth.style.background = "#FF8532";
  10074. }
  10075. switchConfigurations.apply(this);
  10076. radioButton1.onchange = switchConfigurations.bind(this);
  10077. radioButton2.onchange = switchConfigurations.bind(this);
  10078. radioButton3.onchange = switchConfigurations.bind(this);
  10079. }
  10080. },
  10081. /**
  10082. * This overwrites the this.constants.
  10083. *
  10084. * @param constantsVariableName
  10085. * @param value
  10086. * @private
  10087. */
  10088. _overWriteGraphConstants: function (constantsVariableName, value) {
  10089. var nameArray = constantsVariableName.split("_");
  10090. if (nameArray.length == 1) {
  10091. this.constants[nameArray[0]] = value;
  10092. }
  10093. else if (nameArray.length == 2) {
  10094. this.constants[nameArray[0]][nameArray[1]] = value;
  10095. }
  10096. else if (nameArray.length == 3) {
  10097. this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value;
  10098. }
  10099. }
  10100. };
  10101. /**
  10102. * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype.
  10103. */
  10104. function graphToggleSmoothCurves () {
  10105. this.constants.smoothCurves = !this.constants.smoothCurves;
  10106. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10107. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  10108. else {graph_toggleSmooth.style.background = "#FF8532";}
  10109. this._configureSmoothCurves(false);
  10110. };
  10111. /**
  10112. * this function is used to scramble the nodes
  10113. *
  10114. */
  10115. function graphRepositionNodes () {
  10116. for (var nodeId in this.calculationNodes) {
  10117. if (this.calculationNodes.hasOwnProperty(nodeId)) {
  10118. this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0;
  10119. this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0;
  10120. }
  10121. }
  10122. if (this.constants.hierarchicalLayout.enabled == true) {
  10123. this._setupHierarchicalLayout();
  10124. }
  10125. else {
  10126. this.repositionNodes();
  10127. }
  10128. this.moving = true;
  10129. this.start();
  10130. };
  10131. /**
  10132. * this is used to generate an options file from the playing with physics system.
  10133. */
  10134. function graphGenerateOptions () {
  10135. var options = "No options are required, default values used.";
  10136. var optionsSpecific = [];
  10137. var radioButton1 = document.getElementById("graph_physicsMethod1");
  10138. var radioButton2 = document.getElementById("graph_physicsMethod2");
  10139. if (radioButton1.checked == true) {
  10140. if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);}
  10141. if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10142. if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10143. if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10144. if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10145. if (optionsSpecific.length != 0) {
  10146. options = "var options = {";
  10147. options += "physics: {barnesHut: {";
  10148. for (var i = 0; i < optionsSpecific.length; i++) {
  10149. options += optionsSpecific[i];
  10150. if (i < optionsSpecific.length - 1) {
  10151. options += ", "
  10152. }
  10153. }
  10154. options += '}}'
  10155. }
  10156. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10157. if (optionsSpecific.length == 0) {options = "var options = {";}
  10158. else {options += ", "}
  10159. options += "smoothCurves: " + this.constants.smoothCurves;
  10160. }
  10161. if (options != "No options are required, default values used.") {
  10162. options += '};'
  10163. }
  10164. }
  10165. else if (radioButton2.checked == true) {
  10166. options = "var options = {";
  10167. options += "physics: {barnesHut: {enabled: false}";
  10168. if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);}
  10169. if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10170. if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10171. if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10172. if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10173. if (optionsSpecific.length != 0) {
  10174. options += ", repulsion: {";
  10175. for (var i = 0; i < optionsSpecific.length; i++) {
  10176. options += optionsSpecific[i];
  10177. if (i < optionsSpecific.length - 1) {
  10178. options += ", "
  10179. }
  10180. }
  10181. options += '}}'
  10182. }
  10183. if (optionsSpecific.length == 0) {options += "}"}
  10184. if (this.constants.smoothCurves != this.backupConstants.smoothCurves) {
  10185. options += ", smoothCurves: " + this.constants.smoothCurves;
  10186. }
  10187. options += '};'
  10188. }
  10189. else {
  10190. options = "var options = {";
  10191. if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);}
  10192. if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);}
  10193. if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);}
  10194. if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);}
  10195. if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);}
  10196. if (optionsSpecific.length != 0) {
  10197. options += "physics: {hierarchicalRepulsion: {";
  10198. for (var i = 0; i < optionsSpecific.length; i++) {
  10199. options += optionsSpecific[i];
  10200. if (i < optionsSpecific.length - 1) {
  10201. options += ", ";
  10202. }
  10203. }
  10204. options += '}},';
  10205. }
  10206. options += 'hierarchicalLayout: {';
  10207. optionsSpecific = [];
  10208. if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);}
  10209. if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);}
  10210. if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);}
  10211. if (optionsSpecific.length != 0) {
  10212. for (var i = 0; i < optionsSpecific.length; i++) {
  10213. options += optionsSpecific[i];
  10214. if (i < optionsSpecific.length - 1) {
  10215. options += ", "
  10216. }
  10217. }
  10218. options += '}'
  10219. }
  10220. else {
  10221. options += "enabled:true}";
  10222. }
  10223. options += '};'
  10224. }
  10225. this.optionsDiv.innerHTML = options;
  10226. };
  10227. /**
  10228. * this is used to switch between barnesHut, repulsion and hierarchical.
  10229. *
  10230. */
  10231. function switchConfigurations () {
  10232. var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"];
  10233. var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value;
  10234. var tableId = "graph_" + radioButton + "_table";
  10235. var table = document.getElementById(tableId);
  10236. table.style.display = "block";
  10237. for (var i = 0; i < ids.length; i++) {
  10238. if (ids[i] != tableId) {
  10239. table = document.getElementById(ids[i]);
  10240. table.style.display = "none";
  10241. }
  10242. }
  10243. this._restoreNodes();
  10244. if (radioButton == "R") {
  10245. this.constants.hierarchicalLayout.enabled = false;
  10246. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10247. this.constants.physics.barnesHut.enabled = false;
  10248. }
  10249. else if (radioButton == "H") {
  10250. if (this.constants.hierarchicalLayout.enabled == false) {
  10251. this.constants.hierarchicalLayout.enabled = true;
  10252. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10253. this.constants.physics.barnesHut.enabled = false;
  10254. this._setupHierarchicalLayout();
  10255. }
  10256. }
  10257. else {
  10258. this.constants.hierarchicalLayout.enabled = false;
  10259. this.constants.physics.hierarchicalRepulsion.enabled = false;
  10260. this.constants.physics.barnesHut.enabled = true;
  10261. }
  10262. this._loadSelectedForceSolver();
  10263. var graph_toggleSmooth = document.getElementById("graph_toggleSmooth");
  10264. if (this.constants.smoothCurves == true) {graph_toggleSmooth.style.background = "#A4FF56";}
  10265. else {graph_toggleSmooth.style.background = "#FF8532";}
  10266. this.moving = true;
  10267. this.start();
  10268. }
  10269. /**
  10270. * this generates the ranges depending on the iniital values.
  10271. *
  10272. * @param id
  10273. * @param map
  10274. * @param constantsVariableName
  10275. */
  10276. function showValueOfRange (id,map,constantsVariableName) {
  10277. var valueId = id + "_value";
  10278. var rangeValue = document.getElementById(id).value;
  10279. if (map instanceof Array) {
  10280. document.getElementById(valueId).value = map[parseInt(rangeValue)];
  10281. this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]);
  10282. }
  10283. else {
  10284. document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue);
  10285. this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue));
  10286. }
  10287. if (constantsVariableName == "hierarchicalLayout_direction" ||
  10288. constantsVariableName == "hierarchicalLayout_levelSeparation" ||
  10289. constantsVariableName == "hierarchicalLayout_nodeSpacing") {
  10290. this._setupHierarchicalLayout();
  10291. }
  10292. this.moving = true;
  10293. this.start();
  10294. };
  10295. /**
  10296. * Created by Alex on 2/10/14.
  10297. */
  10298. var hierarchalRepulsionMixin = {
  10299. /**
  10300. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10301. * This field is linearly approximated.
  10302. *
  10303. * @private
  10304. */
  10305. _calculateNodeForces: function () {
  10306. var dx, dy, distance, fx, fy, combinedClusterSize,
  10307. repulsingForce, node1, node2, i, j;
  10308. var nodes = this.calculationNodes;
  10309. var nodeIndices = this.calculationNodeIndices;
  10310. // approximation constants
  10311. var b = 5;
  10312. var a_base = 0.5 * -b;
  10313. // repulsing forces between nodes
  10314. var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance;
  10315. var minimumDistance = nodeDistance;
  10316. // we loop from i over all but the last entree in the array
  10317. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10318. for (i = 0; i < nodeIndices.length - 1; i++) {
  10319. node1 = nodes[nodeIndices[i]];
  10320. for (j = i + 1; j < nodeIndices.length; j++) {
  10321. node2 = nodes[nodeIndices[j]];
  10322. dx = node2.x - node1.x;
  10323. dy = node2.y - node1.y;
  10324. distance = Math.sqrt(dx * dx + dy * dy);
  10325. var a = a_base / minimumDistance;
  10326. if (distance < 2 * minimumDistance) {
  10327. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10328. // normalize force with
  10329. if (distance == 0) {
  10330. distance = 0.01;
  10331. }
  10332. else {
  10333. repulsingForce = repulsingForce / distance;
  10334. }
  10335. fx = dx * repulsingForce;
  10336. fy = dy * repulsingForce;
  10337. node1.fx -= fx;
  10338. node1.fy -= fy;
  10339. node2.fx += fx;
  10340. node2.fy += fy;
  10341. }
  10342. }
  10343. }
  10344. }
  10345. };
  10346. /**
  10347. * Created by Alex on 2/10/14.
  10348. */
  10349. var barnesHutMixin = {
  10350. /**
  10351. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  10352. * The Barnes Hut method is used to speed up this N-body simulation.
  10353. *
  10354. * @private
  10355. */
  10356. _calculateNodeForces : function() {
  10357. if (this.constants.physics.barnesHut.gravitationalConstant != 0) {
  10358. var node;
  10359. var nodes = this.calculationNodes;
  10360. var nodeIndices = this.calculationNodeIndices;
  10361. var nodeCount = nodeIndices.length;
  10362. this._formBarnesHutTree(nodes,nodeIndices);
  10363. var barnesHutTree = this.barnesHutTree;
  10364. // place the nodes one by one recursively
  10365. for (var i = 0; i < nodeCount; i++) {
  10366. node = nodes[nodeIndices[i]];
  10367. // starting with root is irrelevant, it never passes the BarnesHut condition
  10368. this._getForceContribution(barnesHutTree.root.children.NW,node);
  10369. this._getForceContribution(barnesHutTree.root.children.NE,node);
  10370. this._getForceContribution(barnesHutTree.root.children.SW,node);
  10371. this._getForceContribution(barnesHutTree.root.children.SE,node);
  10372. }
  10373. }
  10374. },
  10375. /**
  10376. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  10377. * If a region contains a single node, we check if it is not itself, then we apply the force.
  10378. *
  10379. * @param parentBranch
  10380. * @param node
  10381. * @private
  10382. */
  10383. _getForceContribution : function(parentBranch,node) {
  10384. // we get no force contribution from an empty region
  10385. if (parentBranch.childrenCount > 0) {
  10386. var dx,dy,distance;
  10387. // get the distance from the center of mass to the node.
  10388. dx = parentBranch.centerOfMass.x - node.x;
  10389. dy = parentBranch.centerOfMass.y - node.y;
  10390. distance = Math.sqrt(dx * dx + dy * dy);
  10391. // BarnesHut condition
  10392. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  10393. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  10394. if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.theta) {
  10395. // duplicate code to reduce function calls to speed up program
  10396. if (distance == 0) {
  10397. distance = 0.1*Math.random();
  10398. dx = distance;
  10399. }
  10400. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10401. var fx = dx * gravityForce;
  10402. var fy = dy * gravityForce;
  10403. node.fx += fx;
  10404. node.fy += fy;
  10405. }
  10406. else {
  10407. // Did not pass the condition, go into children if available
  10408. if (parentBranch.childrenCount == 4) {
  10409. this._getForceContribution(parentBranch.children.NW,node);
  10410. this._getForceContribution(parentBranch.children.NE,node);
  10411. this._getForceContribution(parentBranch.children.SW,node);
  10412. this._getForceContribution(parentBranch.children.SE,node);
  10413. }
  10414. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  10415. if (parentBranch.children.data.id != node.id) { // if it is not self
  10416. // duplicate code to reduce function calls to speed up program
  10417. if (distance == 0) {
  10418. distance = 0.5*Math.random();
  10419. dx = distance;
  10420. }
  10421. var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.mass / (distance * distance * distance);
  10422. var fx = dx * gravityForce;
  10423. var fy = dy * gravityForce;
  10424. node.fx += fx;
  10425. node.fy += fy;
  10426. }
  10427. }
  10428. }
  10429. }
  10430. },
  10431. /**
  10432. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  10433. *
  10434. * @param nodes
  10435. * @param nodeIndices
  10436. * @private
  10437. */
  10438. _formBarnesHutTree : function(nodes,nodeIndices) {
  10439. var node;
  10440. var nodeCount = nodeIndices.length;
  10441. var minX = Number.MAX_VALUE,
  10442. minY = Number.MAX_VALUE,
  10443. maxX =-Number.MAX_VALUE,
  10444. maxY =-Number.MAX_VALUE;
  10445. // get the range of the nodes
  10446. for (var i = 0; i < nodeCount; i++) {
  10447. var x = nodes[nodeIndices[i]].x;
  10448. var y = nodes[nodeIndices[i]].y;
  10449. if (x < minX) { minX = x; }
  10450. if (x > maxX) { maxX = x; }
  10451. if (y < minY) { minY = y; }
  10452. if (y > maxY) { maxY = y; }
  10453. }
  10454. // make the range a square
  10455. var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  10456. if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
  10457. else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
  10458. var minimumTreeSize = 1e-5;
  10459. var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
  10460. var halfRootSize = 0.5 * rootSize;
  10461. var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  10462. // construct the barnesHutTree
  10463. var barnesHutTree = {root:{
  10464. centerOfMass:{x:0,y:0}, // Center of Mass
  10465. mass:0,
  10466. range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize,
  10467. minY:centerY-halfRootSize,maxY:centerY+halfRootSize},
  10468. size: rootSize,
  10469. calcSize: 1 / rootSize,
  10470. children: {data:null},
  10471. maxWidth: 0,
  10472. level: 0,
  10473. childrenCount: 4
  10474. }};
  10475. this._splitBranch(barnesHutTree.root);
  10476. // place the nodes one by one recursively
  10477. for (i = 0; i < nodeCount; i++) {
  10478. node = nodes[nodeIndices[i]];
  10479. this._placeInTree(barnesHutTree.root,node);
  10480. }
  10481. // make global
  10482. this.barnesHutTree = barnesHutTree
  10483. },
  10484. /**
  10485. * this updates the mass of a branch. this is increased by adding a node.
  10486. *
  10487. * @param parentBranch
  10488. * @param node
  10489. * @private
  10490. */
  10491. _updateBranchMass : function(parentBranch, node) {
  10492. var totalMass = parentBranch.mass + node.mass;
  10493. var totalMassInv = 1/totalMass;
  10494. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.mass;
  10495. parentBranch.centerOfMass.x *= totalMassInv;
  10496. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.mass;
  10497. parentBranch.centerOfMass.y *= totalMassInv;
  10498. parentBranch.mass = totalMass;
  10499. var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
  10500. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  10501. },
  10502. /**
  10503. * determine in which branch the node will be placed.
  10504. *
  10505. * @param parentBranch
  10506. * @param node
  10507. * @param skipMassUpdate
  10508. * @private
  10509. */
  10510. _placeInTree : function(parentBranch,node,skipMassUpdate) {
  10511. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  10512. // update the mass of the branch.
  10513. this._updateBranchMass(parentBranch,node);
  10514. }
  10515. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  10516. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  10517. this._placeInRegion(parentBranch,node,"NW");
  10518. }
  10519. else { // in SW
  10520. this._placeInRegion(parentBranch,node,"SW");
  10521. }
  10522. }
  10523. else { // in NE or SE
  10524. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  10525. this._placeInRegion(parentBranch,node,"NE");
  10526. }
  10527. else { // in SE
  10528. this._placeInRegion(parentBranch,node,"SE");
  10529. }
  10530. }
  10531. },
  10532. /**
  10533. * actually place the node in a region (or branch)
  10534. *
  10535. * @param parentBranch
  10536. * @param node
  10537. * @param region
  10538. * @private
  10539. */
  10540. _placeInRegion : function(parentBranch,node,region) {
  10541. switch (parentBranch.children[region].childrenCount) {
  10542. case 0: // place node here
  10543. parentBranch.children[region].children.data = node;
  10544. parentBranch.children[region].childrenCount = 1;
  10545. this._updateBranchMass(parentBranch.children[region],node);
  10546. break;
  10547. case 1: // convert into children
  10548. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  10549. // we move one node a pixel and we do not put it in the tree.
  10550. if (parentBranch.children[region].children.data.x == node.x &&
  10551. parentBranch.children[region].children.data.y == node.y) {
  10552. node.x += Math.random();
  10553. node.y += Math.random();
  10554. }
  10555. else {
  10556. this._splitBranch(parentBranch.children[region]);
  10557. this._placeInTree(parentBranch.children[region],node);
  10558. }
  10559. break;
  10560. case 4: // place in branch
  10561. this._placeInTree(parentBranch.children[region],node);
  10562. break;
  10563. }
  10564. },
  10565. /**
  10566. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  10567. * after the split is complete.
  10568. *
  10569. * @param parentBranch
  10570. * @private
  10571. */
  10572. _splitBranch : function(parentBranch) {
  10573. // if the branch is filled with a node, replace the node in the new subset.
  10574. var containedNode = null;
  10575. if (parentBranch.childrenCount == 1) {
  10576. containedNode = parentBranch.children.data;
  10577. parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
  10578. }
  10579. parentBranch.childrenCount = 4;
  10580. parentBranch.children.data = null;
  10581. this._insertRegion(parentBranch,"NW");
  10582. this._insertRegion(parentBranch,"NE");
  10583. this._insertRegion(parentBranch,"SW");
  10584. this._insertRegion(parentBranch,"SE");
  10585. if (containedNode != null) {
  10586. this._placeInTree(parentBranch,containedNode);
  10587. }
  10588. },
  10589. /**
  10590. * This function subdivides the region into four new segments.
  10591. * Specifically, this inserts a single new segment.
  10592. * It fills the children section of the parentBranch
  10593. *
  10594. * @param parentBranch
  10595. * @param region
  10596. * @param parentRange
  10597. * @private
  10598. */
  10599. _insertRegion : function(parentBranch, region) {
  10600. var minX,maxX,minY,maxY;
  10601. var childSize = 0.5 * parentBranch.size;
  10602. switch (region) {
  10603. case "NW":
  10604. minX = parentBranch.range.minX;
  10605. maxX = parentBranch.range.minX + childSize;
  10606. minY = parentBranch.range.minY;
  10607. maxY = parentBranch.range.minY + childSize;
  10608. break;
  10609. case "NE":
  10610. minX = parentBranch.range.minX + childSize;
  10611. maxX = parentBranch.range.maxX;
  10612. minY = parentBranch.range.minY;
  10613. maxY = parentBranch.range.minY + childSize;
  10614. break;
  10615. case "SW":
  10616. minX = parentBranch.range.minX;
  10617. maxX = parentBranch.range.minX + childSize;
  10618. minY = parentBranch.range.minY + childSize;
  10619. maxY = parentBranch.range.maxY;
  10620. break;
  10621. case "SE":
  10622. minX = parentBranch.range.minX + childSize;
  10623. maxX = parentBranch.range.maxX;
  10624. minY = parentBranch.range.minY + childSize;
  10625. maxY = parentBranch.range.maxY;
  10626. break;
  10627. }
  10628. parentBranch.children[region] = {
  10629. centerOfMass:{x:0,y:0},
  10630. mass:0,
  10631. range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
  10632. size: 0.5 * parentBranch.size,
  10633. calcSize: 2 * parentBranch.calcSize,
  10634. children: {data:null},
  10635. maxWidth: 0,
  10636. level: parentBranch.level+1,
  10637. childrenCount: 0
  10638. };
  10639. },
  10640. /**
  10641. * This function is for debugging purposed, it draws the tree.
  10642. *
  10643. * @param ctx
  10644. * @param color
  10645. * @private
  10646. */
  10647. _drawTree : function(ctx,color) {
  10648. if (this.barnesHutTree !== undefined) {
  10649. ctx.lineWidth = 1;
  10650. this._drawBranch(this.barnesHutTree.root,ctx,color);
  10651. }
  10652. },
  10653. /**
  10654. * This function is for debugging purposes. It draws the branches recursively.
  10655. *
  10656. * @param branch
  10657. * @param ctx
  10658. * @param color
  10659. * @private
  10660. */
  10661. _drawBranch : function(branch,ctx,color) {
  10662. if (color === undefined) {
  10663. color = "#FF0000";
  10664. }
  10665. if (branch.childrenCount == 4) {
  10666. this._drawBranch(branch.children.NW,ctx);
  10667. this._drawBranch(branch.children.NE,ctx);
  10668. this._drawBranch(branch.children.SE,ctx);
  10669. this._drawBranch(branch.children.SW,ctx);
  10670. }
  10671. ctx.strokeStyle = color;
  10672. ctx.beginPath();
  10673. ctx.moveTo(branch.range.minX,branch.range.minY);
  10674. ctx.lineTo(branch.range.maxX,branch.range.minY);
  10675. ctx.stroke();
  10676. ctx.beginPath();
  10677. ctx.moveTo(branch.range.maxX,branch.range.minY);
  10678. ctx.lineTo(branch.range.maxX,branch.range.maxY);
  10679. ctx.stroke();
  10680. ctx.beginPath();
  10681. ctx.moveTo(branch.range.maxX,branch.range.maxY);
  10682. ctx.lineTo(branch.range.minX,branch.range.maxY);
  10683. ctx.stroke();
  10684. ctx.beginPath();
  10685. ctx.moveTo(branch.range.minX,branch.range.maxY);
  10686. ctx.lineTo(branch.range.minX,branch.range.minY);
  10687. ctx.stroke();
  10688. /*
  10689. if (branch.mass > 0) {
  10690. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  10691. ctx.stroke();
  10692. }
  10693. */
  10694. }
  10695. };
  10696. /**
  10697. * Created by Alex on 2/10/14.
  10698. */
  10699. var repulsionMixin = {
  10700. /**
  10701. * Calculate the forces the nodes apply on eachother based on a repulsion field.
  10702. * This field is linearly approximated.
  10703. *
  10704. * @private
  10705. */
  10706. _calculateNodeForces: function () {
  10707. var dx, dy, angle, distance, fx, fy, combinedClusterSize,
  10708. repulsingForce, node1, node2, i, j;
  10709. var nodes = this.calculationNodes;
  10710. var nodeIndices = this.calculationNodeIndices;
  10711. // approximation constants
  10712. var a_base = -2 / 3;
  10713. var b = 4 / 3;
  10714. // repulsing forces between nodes
  10715. var nodeDistance = this.constants.physics.repulsion.nodeDistance;
  10716. var minimumDistance = nodeDistance;
  10717. // we loop from i over all but the last entree in the array
  10718. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  10719. for (i = 0; i < nodeIndices.length - 1; i++) {
  10720. node1 = nodes[nodeIndices[i]];
  10721. for (j = i + 1; j < nodeIndices.length; j++) {
  10722. node2 = nodes[nodeIndices[j]];
  10723. combinedClusterSize = node1.clusterSize + node2.clusterSize - 2;
  10724. dx = node2.x - node1.x;
  10725. dy = node2.y - node1.y;
  10726. distance = Math.sqrt(dx * dx + dy * dy);
  10727. minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification));
  10728. var a = a_base / minimumDistance;
  10729. if (distance < 2 * minimumDistance) {
  10730. if (distance < 0.5 * minimumDistance) {
  10731. repulsingForce = 1.0;
  10732. }
  10733. else {
  10734. repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness))
  10735. }
  10736. // amplify the repulsion for clusters.
  10737. repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification;
  10738. repulsingForce = repulsingForce / distance;
  10739. fx = dx * repulsingForce;
  10740. fy = dy * repulsingForce;
  10741. node1.fx -= fx;
  10742. node1.fy -= fy;
  10743. node2.fx += fx;
  10744. node2.fy += fy;
  10745. }
  10746. }
  10747. }
  10748. }
  10749. };
  10750. var HierarchicalLayoutMixin = {
  10751. _resetLevels : function() {
  10752. for (var nodeId in this.nodes) {
  10753. if (this.nodes.hasOwnProperty(nodeId)) {
  10754. var node = this.nodes[nodeId];
  10755. if (node.preassignedLevel == false) {
  10756. node.level = -1;
  10757. }
  10758. }
  10759. }
  10760. },
  10761. /**
  10762. * This is the main function to layout the nodes in a hierarchical way.
  10763. * It checks if the node details are supplied correctly
  10764. *
  10765. * @private
  10766. */
  10767. _setupHierarchicalLayout : function() {
  10768. if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
  10769. if (this.constants.hierarchicalLayout.direction == "RL" || this.constants.hierarchicalLayout.direction == "DU") {
  10770. this.constants.hierarchicalLayout.levelSeparation *= -1;
  10771. }
  10772. else {
  10773. this.constants.hierarchicalLayout.levelSeparation = Math.abs(this.constants.hierarchicalLayout.levelSeparation);
  10774. }
  10775. // get the size of the largest hubs and check if the user has defined a level for a node.
  10776. var hubsize = 0;
  10777. var node, nodeId;
  10778. var definedLevel = false;
  10779. var undefinedLevel = false;
  10780. for (nodeId in this.nodes) {
  10781. if (this.nodes.hasOwnProperty(nodeId)) {
  10782. node = this.nodes[nodeId];
  10783. if (node.level != -1) {
  10784. definedLevel = true;
  10785. }
  10786. else {
  10787. undefinedLevel = true;
  10788. }
  10789. if (hubsize < node.edges.length) {
  10790. hubsize = node.edges.length;
  10791. }
  10792. }
  10793. }
  10794. // if the user defined some levels but not all, alert and run without hierarchical layout
  10795. if (undefinedLevel == true && definedLevel == true) {
  10796. alert("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
  10797. this.zoomExtent(true,this.constants.clustering.enabled);
  10798. if (!this.constants.clustering.enabled) {
  10799. this.start();
  10800. }
  10801. }
  10802. else {
  10803. // setup the system to use hierarchical method.
  10804. this._changeConstants();
  10805. // define levels if undefined by the users. Based on hubsize
  10806. if (undefinedLevel == true) {
  10807. this._determineLevels(hubsize);
  10808. }
  10809. // check the distribution of the nodes per level.
  10810. var distribution = this._getDistribution();
  10811. // place the nodes on the canvas. This also stablilizes the system.
  10812. this._placeNodesByHierarchy(distribution);
  10813. // start the simulation.
  10814. this.start();
  10815. }
  10816. }
  10817. },
  10818. /**
  10819. * This function places the nodes on the canvas based on the hierarchial distribution.
  10820. *
  10821. * @param {Object} distribution | obtained by the function this._getDistribution()
  10822. * @private
  10823. */
  10824. _placeNodesByHierarchy : function(distribution) {
  10825. var nodeId, node;
  10826. // start placing all the level 0 nodes first. Then recursively position their branches.
  10827. for (nodeId in distribution[0].nodes) {
  10828. if (distribution[0].nodes.hasOwnProperty(nodeId)) {
  10829. node = distribution[0].nodes[nodeId];
  10830. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10831. if (node.xFixed) {
  10832. node.x = distribution[0].minPos;
  10833. node.xFixed = false;
  10834. distribution[0].minPos += distribution[0].nodeSpacing;
  10835. }
  10836. }
  10837. else {
  10838. if (node.yFixed) {
  10839. node.y = distribution[0].minPos;
  10840. node.yFixed = false;
  10841. distribution[0].minPos += distribution[0].nodeSpacing;
  10842. }
  10843. }
  10844. this._placeBranchNodes(node.edges,node.id,distribution,node.level);
  10845. }
  10846. }
  10847. // stabilize the system after positioning. This function calls zoomExtent.
  10848. this._stabilize();
  10849. },
  10850. /**
  10851. * This function get the distribution of levels based on hubsize
  10852. *
  10853. * @returns {Object}
  10854. * @private
  10855. */
  10856. _getDistribution : function() {
  10857. var distribution = {};
  10858. var nodeId, node, level;
  10859. // 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.
  10860. // the fix of X is removed after the x value has been set.
  10861. for (nodeId in this.nodes) {
  10862. if (this.nodes.hasOwnProperty(nodeId)) {
  10863. node = this.nodes[nodeId];
  10864. node.xFixed = true;
  10865. node.yFixed = true;
  10866. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10867. node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10868. }
  10869. else {
  10870. node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
  10871. }
  10872. if (!distribution.hasOwnProperty(node.level)) {
  10873. distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
  10874. }
  10875. distribution[node.level].amount += 1;
  10876. distribution[node.level].nodes[node.id] = node;
  10877. }
  10878. }
  10879. // determine the largest amount of nodes of all levels
  10880. var maxCount = 0;
  10881. for (level in distribution) {
  10882. if (distribution.hasOwnProperty(level)) {
  10883. if (maxCount < distribution[level].amount) {
  10884. maxCount = distribution[level].amount;
  10885. }
  10886. }
  10887. }
  10888. // set the initial position and spacing of each nodes accordingly
  10889. for (level in distribution) {
  10890. if (distribution.hasOwnProperty(level)) {
  10891. distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
  10892. distribution[level].nodeSpacing /= (distribution[level].amount + 1);
  10893. distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
  10894. }
  10895. }
  10896. return distribution;
  10897. },
  10898. /**
  10899. * this function allocates nodes in levels based on the recursive branching from the largest hubs.
  10900. *
  10901. * @param hubsize
  10902. * @private
  10903. */
  10904. _determineLevels : function(hubsize) {
  10905. var nodeId, node;
  10906. // determine hubs
  10907. for (nodeId in this.nodes) {
  10908. if (this.nodes.hasOwnProperty(nodeId)) {
  10909. node = this.nodes[nodeId];
  10910. if (node.edges.length == hubsize) {
  10911. node.level = 0;
  10912. }
  10913. }
  10914. }
  10915. // branch from hubs
  10916. for (nodeId in this.nodes) {
  10917. if (this.nodes.hasOwnProperty(nodeId)) {
  10918. node = this.nodes[nodeId];
  10919. if (node.level == 0) {
  10920. this._setLevel(1,node.edges,node.id);
  10921. }
  10922. }
  10923. }
  10924. },
  10925. /**
  10926. * Since hierarchical layout does not support:
  10927. * - smooth curves (based on the physics),
  10928. * - clustering (based on dynamic node counts)
  10929. *
  10930. * We disable both features so there will be no problems.
  10931. *
  10932. * @private
  10933. */
  10934. _changeConstants : function() {
  10935. this.constants.clustering.enabled = false;
  10936. this.constants.physics.barnesHut.enabled = false;
  10937. this.constants.physics.hierarchicalRepulsion.enabled = true;
  10938. this._loadSelectedForceSolver();
  10939. this.constants.smoothCurves = false;
  10940. this._configureSmoothCurves();
  10941. },
  10942. /**
  10943. * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
  10944. * on a X position that ensures there will be no overlap.
  10945. *
  10946. * @param edges
  10947. * @param parentId
  10948. * @param distribution
  10949. * @param parentLevel
  10950. * @private
  10951. */
  10952. _placeBranchNodes : function(edges, parentId, distribution, parentLevel) {
  10953. for (var i = 0; i < edges.length; i++) {
  10954. var childNode = null;
  10955. if (edges[i].toId == parentId) {
  10956. childNode = edges[i].from;
  10957. }
  10958. else {
  10959. childNode = edges[i].to;
  10960. }
  10961. // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
  10962. var nodeMoved = false;
  10963. if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
  10964. if (childNode.xFixed && childNode.level > parentLevel) {
  10965. childNode.xFixed = false;
  10966. childNode.x = distribution[childNode.level].minPos;
  10967. nodeMoved = true;
  10968. }
  10969. }
  10970. else {
  10971. if (childNode.yFixed && childNode.level > parentLevel) {
  10972. childNode.yFixed = false;
  10973. childNode.y = distribution[childNode.level].minPos;
  10974. nodeMoved = true;
  10975. }
  10976. }
  10977. if (nodeMoved == true) {
  10978. distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
  10979. if (childNode.edges.length > 1) {
  10980. this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
  10981. }
  10982. }
  10983. }
  10984. },
  10985. /**
  10986. * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
  10987. *
  10988. * @param level
  10989. * @param edges
  10990. * @param parentId
  10991. * @private
  10992. */
  10993. _setLevel : function(level, edges, parentId) {
  10994. for (var i = 0; i < edges.length; i++) {
  10995. var childNode = null;
  10996. if (edges[i].toId == parentId) {
  10997. childNode = edges[i].from;
  10998. }
  10999. else {
  11000. childNode = edges[i].to;
  11001. }
  11002. if (childNode.level == -1 || childNode.level > level) {
  11003. childNode.level = level;
  11004. if (edges.length > 1) {
  11005. this._setLevel(level+1, childNode.edges, childNode.id);
  11006. }
  11007. }
  11008. }
  11009. },
  11010. /**
  11011. * Unfix nodes
  11012. *
  11013. * @private
  11014. */
  11015. _restoreNodes : function() {
  11016. for (nodeId in this.nodes) {
  11017. if (this.nodes.hasOwnProperty(nodeId)) {
  11018. this.nodes[nodeId].xFixed = false;
  11019. this.nodes[nodeId].yFixed = false;
  11020. }
  11021. }
  11022. }
  11023. };
  11024. /**
  11025. * Created by Alex on 2/4/14.
  11026. */
  11027. var manipulationMixin = {
  11028. /**
  11029. * clears the toolbar div element of children
  11030. *
  11031. * @private
  11032. */
  11033. _clearManipulatorBar : function() {
  11034. while (this.manipulationDiv.hasChildNodes()) {
  11035. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11036. }
  11037. },
  11038. /**
  11039. * Manipulation UI temporarily overloads certain functions to extend or replace them. To be able to restore
  11040. * these functions to their original functionality, we saved them in this.cachedFunctions.
  11041. * This function restores these functions to their original function.
  11042. *
  11043. * @private
  11044. */
  11045. _restoreOverloadedFunctions : function() {
  11046. for (var functionName in this.cachedFunctions) {
  11047. if (this.cachedFunctions.hasOwnProperty(functionName)) {
  11048. this[functionName] = this.cachedFunctions[functionName];
  11049. }
  11050. }
  11051. },
  11052. /**
  11053. * Enable or disable edit-mode.
  11054. *
  11055. * @private
  11056. */
  11057. _toggleEditMode : function() {
  11058. this.editMode = !this.editMode;
  11059. var toolbar = document.getElementById("graph-manipulationDiv");
  11060. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11061. var editModeDiv = document.getElementById("graph-manipulation-editMode");
  11062. if (this.editMode == true) {
  11063. toolbar.style.display="block";
  11064. closeDiv.style.display="block";
  11065. editModeDiv.style.display="none";
  11066. closeDiv.onclick = this._toggleEditMode.bind(this);
  11067. }
  11068. else {
  11069. toolbar.style.display="none";
  11070. closeDiv.style.display="none";
  11071. editModeDiv.style.display="block";
  11072. closeDiv.onclick = null;
  11073. }
  11074. this._createManipulatorBar()
  11075. },
  11076. /**
  11077. * main function, creates the main toolbar. Removes functions bound to the select event. Binds all the buttons of the toolbar.
  11078. *
  11079. * @private
  11080. */
  11081. _createManipulatorBar : function() {
  11082. // remove bound functions
  11083. if (this.boundFunction) {
  11084. this.off('select', this.boundFunction);
  11085. }
  11086. // restore overloaded functions
  11087. this._restoreOverloadedFunctions();
  11088. // resume calculation
  11089. this.freezeSimulation = false;
  11090. // reset global variables
  11091. this.blockConnectingEdgeSelection = false;
  11092. this.forceAppendSelection = false;
  11093. if (this.editMode == true) {
  11094. while (this.manipulationDiv.hasChildNodes()) {
  11095. this.manipulationDiv.removeChild(this.manipulationDiv.firstChild);
  11096. }
  11097. // add the icons to the manipulator div
  11098. this.manipulationDiv.innerHTML = "" +
  11099. "<span class='graph-manipulationUI add' id='graph-manipulate-addNode'>" +
  11100. "<span class='graph-manipulationLabel'>"+this.constants.labels['add'] +"</span></span>" +
  11101. "<div class='graph-seperatorLine'></div>" +
  11102. "<span class='graph-manipulationUI connect' id='graph-manipulate-connectNode'>" +
  11103. "<span class='graph-manipulationLabel'>"+this.constants.labels['link'] +"</span></span>";
  11104. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11105. this.manipulationDiv.innerHTML += "" +
  11106. "<div class='graph-seperatorLine'></div>" +
  11107. "<span class='graph-manipulationUI edit' id='graph-manipulate-editNode'>" +
  11108. "<span class='graph-manipulationLabel'>"+this.constants.labels['editNode'] +"</span></span>";
  11109. }
  11110. if (this._selectionIsEmpty() == false) {
  11111. this.manipulationDiv.innerHTML += "" +
  11112. "<div class='graph-seperatorLine'></div>" +
  11113. "<span class='graph-manipulationUI delete' id='graph-manipulate-delete'>" +
  11114. "<span class='graph-manipulationLabel'>"+this.constants.labels['del'] +"</span></span>";
  11115. }
  11116. // bind the icons
  11117. var addNodeButton = document.getElementById("graph-manipulate-addNode");
  11118. addNodeButton.onclick = this._createAddNodeToolbar.bind(this);
  11119. var addEdgeButton = document.getElementById("graph-manipulate-connectNode");
  11120. addEdgeButton.onclick = this._createAddEdgeToolbar.bind(this);
  11121. if (this._getSelectedNodeCount() == 1 && this.triggerFunctions.edit) {
  11122. var editButton = document.getElementById("graph-manipulate-editNode");
  11123. editButton.onclick = this._editNode.bind(this);
  11124. }
  11125. if (this._selectionIsEmpty() == false) {
  11126. var deleteButton = document.getElementById("graph-manipulate-delete");
  11127. deleteButton.onclick = this._deleteSelected.bind(this);
  11128. }
  11129. var closeDiv = document.getElementById("graph-manipulation-closeDiv");
  11130. closeDiv.onclick = this._toggleEditMode.bind(this);
  11131. this.boundFunction = this._createManipulatorBar.bind(this);
  11132. this.on('select', this.boundFunction);
  11133. }
  11134. else {
  11135. this.editModeDiv.innerHTML = "" +
  11136. "<span class='graph-manipulationUI edit editmode' id='graph-manipulate-editModeButton'>" +
  11137. "<span class='graph-manipulationLabel'>" + this.constants.labels['edit'] + "</span></span>";
  11138. var editModeButton = document.getElementById("graph-manipulate-editModeButton");
  11139. editModeButton.onclick = this._toggleEditMode.bind(this);
  11140. }
  11141. },
  11142. /**
  11143. * Create the toolbar for adding Nodes
  11144. *
  11145. * @private
  11146. */
  11147. _createAddNodeToolbar : function() {
  11148. // clear the toolbar
  11149. this._clearManipulatorBar();
  11150. if (this.boundFunction) {
  11151. this.off('select', this.boundFunction);
  11152. }
  11153. // create the toolbar contents
  11154. this.manipulationDiv.innerHTML = "" +
  11155. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11156. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11157. "<div class='graph-seperatorLine'></div>" +
  11158. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11159. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['addDescription'] + "</span></span>";
  11160. // bind the icon
  11161. var backButton = document.getElementById("graph-manipulate-back");
  11162. backButton.onclick = this._createManipulatorBar.bind(this);
  11163. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11164. this.boundFunction = this._addNode.bind(this);
  11165. this.on('select', this.boundFunction);
  11166. },
  11167. /**
  11168. * create the toolbar to connect nodes
  11169. *
  11170. * @private
  11171. */
  11172. _createAddEdgeToolbar : function() {
  11173. // clear the toolbar
  11174. this._clearManipulatorBar();
  11175. this._unselectAll(true);
  11176. this.freezeSimulation = true;
  11177. if (this.boundFunction) {
  11178. this.off('select', this.boundFunction);
  11179. }
  11180. this._unselectAll();
  11181. this.forceAppendSelection = false;
  11182. this.blockConnectingEdgeSelection = true;
  11183. this.manipulationDiv.innerHTML = "" +
  11184. "<span class='graph-manipulationUI back' id='graph-manipulate-back'>" +
  11185. "<span class='graph-manipulationLabel'>" + this.constants.labels['back'] + " </span></span>" +
  11186. "<div class='graph-seperatorLine'></div>" +
  11187. "<span class='graph-manipulationUI none' id='graph-manipulate-back'>" +
  11188. "<span id='graph-manipulatorLabel' class='graph-manipulationLabel'>" + this.constants.labels['linkDescription'] + "</span></span>";
  11189. // bind the icon
  11190. var backButton = document.getElementById("graph-manipulate-back");
  11191. backButton.onclick = this._createManipulatorBar.bind(this);
  11192. // we use the boundFunction so we can reference it when we unbind it from the "select" event.
  11193. this.boundFunction = this._handleConnect.bind(this);
  11194. this.on('select', this.boundFunction);
  11195. // temporarily overload functions
  11196. this.cachedFunctions["_handleTouch"] = this._handleTouch;
  11197. this.cachedFunctions["_handleOnRelease"] = this._handleOnRelease;
  11198. this._handleTouch = this._handleConnect;
  11199. this._handleOnRelease = this._finishConnect;
  11200. // redraw to show the unselect
  11201. this._redraw();
  11202. },
  11203. /**
  11204. * the function bound to the selection event. It checks if you want to connect a cluster and changes the description
  11205. * to walk the user through the process.
  11206. *
  11207. * @private
  11208. */
  11209. _handleConnect : function(pointer) {
  11210. if (this._getSelectedNodeCount() == 0) {
  11211. var node = this._getNodeAt(pointer);
  11212. if (node != null) {
  11213. if (node.clusterSize > 1) {
  11214. alert("Cannot create edges to a cluster.")
  11215. }
  11216. else {
  11217. this._selectObject(node,false);
  11218. // create a node the temporary line can look at
  11219. this.sectors['support']['nodes']['targetNode'] = new Node({id:'targetNode'},{},{},this.constants);
  11220. this.sectors['support']['nodes']['targetNode'].x = node.x;
  11221. this.sectors['support']['nodes']['targetNode'].y = node.y;
  11222. this.sectors['support']['nodes']['targetViaNode'] = new Node({id:'targetViaNode'},{},{},this.constants);
  11223. this.sectors['support']['nodes']['targetViaNode'].x = node.x;
  11224. this.sectors['support']['nodes']['targetViaNode'].y = node.y;
  11225. this.sectors['support']['nodes']['targetViaNode'].parentEdgeId = "connectionEdge";
  11226. // create a temporary edge
  11227. this.edges['connectionEdge'] = new Edge({id:"connectionEdge",from:node.id,to:this.sectors['support']['nodes']['targetNode'].id}, this, this.constants);
  11228. this.edges['connectionEdge'].from = node;
  11229. this.edges['connectionEdge'].connected = true;
  11230. this.edges['connectionEdge'].smooth = true;
  11231. this.edges['connectionEdge'].selected = true;
  11232. this.edges['connectionEdge'].to = this.sectors['support']['nodes']['targetNode'];
  11233. this.edges['connectionEdge'].via = this.sectors['support']['nodes']['targetViaNode'];
  11234. this.cachedFunctions["_handleOnDrag"] = this._handleOnDrag;
  11235. this._handleOnDrag = function(event) {
  11236. var pointer = this._getPointer(event.gesture.center);
  11237. this.sectors['support']['nodes']['targetNode'].x = this._XconvertDOMtoCanvas(pointer.x);
  11238. this.sectors['support']['nodes']['targetNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  11239. this.sectors['support']['nodes']['targetViaNode'].x = 0.5 * (this._XconvertDOMtoCanvas(pointer.x) + this.edges['connectionEdge'].from.x);
  11240. this.sectors['support']['nodes']['targetViaNode'].y = this._YconvertDOMtoCanvas(pointer.y);
  11241. };
  11242. this.moving = true;
  11243. this.start();
  11244. }
  11245. }
  11246. }
  11247. },
  11248. _finishConnect : function(pointer) {
  11249. if (this._getSelectedNodeCount() == 1) {
  11250. // restore the drag function
  11251. this._handleOnDrag = this.cachedFunctions["_handleOnDrag"];
  11252. delete this.cachedFunctions["_handleOnDrag"];
  11253. // remember the edge id
  11254. var connectFromId = this.edges['connectionEdge'].fromId;
  11255. // remove the temporary nodes and edge
  11256. delete this.edges['connectionEdge'];
  11257. delete this.sectors['support']['nodes']['targetNode'];
  11258. delete this.sectors['support']['nodes']['targetViaNode'];
  11259. var node = this._getNodeAt(pointer);
  11260. if (node != null) {
  11261. if (node.clusterSize > 1) {
  11262. alert("Cannot create edges to a cluster.")
  11263. }
  11264. else {
  11265. this._createEdge(connectFromId,node.id);
  11266. this._createManipulatorBar();
  11267. }
  11268. }
  11269. this._unselectAll();
  11270. }
  11271. },
  11272. /**
  11273. * Adds a node on the specified location
  11274. */
  11275. _addNode : function() {
  11276. if (this._selectionIsEmpty() && this.editMode == true) {
  11277. var positionObject = this._pointerToPositionObject(this.pointerPosition);
  11278. var defaultData = {id:util.randomUUID(),x:positionObject.left,y:positionObject.top,label:"new",allowedToMoveX:true,allowedToMoveY:true};
  11279. if (this.triggerFunctions.add) {
  11280. if (this.triggerFunctions.add.length == 2) {
  11281. var me = this;
  11282. this.triggerFunctions.add(defaultData, function(finalizedData) {
  11283. me.nodesData.add(finalizedData);
  11284. me._createManipulatorBar();
  11285. me.moving = true;
  11286. me.start();
  11287. });
  11288. }
  11289. else {
  11290. alert(this.constants.labels['addError']);
  11291. this._createManipulatorBar();
  11292. this.moving = true;
  11293. this.start();
  11294. }
  11295. }
  11296. else {
  11297. this.nodesData.add(defaultData);
  11298. this._createManipulatorBar();
  11299. this.moving = true;
  11300. this.start();
  11301. }
  11302. }
  11303. },
  11304. /**
  11305. * connect two nodes with a new edge.
  11306. *
  11307. * @private
  11308. */
  11309. _createEdge : function(sourceNodeId,targetNodeId) {
  11310. if (this.editMode == true) {
  11311. var defaultData = {from:sourceNodeId, to:targetNodeId};
  11312. if (this.triggerFunctions.connect) {
  11313. if (this.triggerFunctions.connect.length == 2) {
  11314. var me = this;
  11315. this.triggerFunctions.connect(defaultData, function(finalizedData) {
  11316. me.edgesData.add(finalizedData);
  11317. me.moving = true;
  11318. me.start();
  11319. });
  11320. }
  11321. else {
  11322. alert(this.constants.labels["linkError"]);
  11323. this.moving = true;
  11324. this.start();
  11325. }
  11326. }
  11327. else {
  11328. this.edgesData.add(defaultData);
  11329. this.moving = true;
  11330. this.start();
  11331. }
  11332. }
  11333. },
  11334. /**
  11335. * Create the toolbar to edit the selected node. The label and the color can be changed. Other colors are derived from the chosen color.
  11336. *
  11337. * @private
  11338. */
  11339. _editNode : function() {
  11340. if (this.triggerFunctions.edit && this.editMode == true) {
  11341. var node = this._getSelectedNode();
  11342. var data = {id:node.id,
  11343. label: node.label,
  11344. group: node.group,
  11345. shape: node.shape,
  11346. color: {
  11347. background:node.color.background,
  11348. border:node.color.border,
  11349. highlight: {
  11350. background:node.color.highlight.background,
  11351. border:node.color.highlight.border
  11352. }
  11353. }};
  11354. if (this.triggerFunctions.edit.length == 2) {
  11355. var me = this;
  11356. this.triggerFunctions.edit(data, function (finalizedData) {
  11357. me.nodesData.update(finalizedData);
  11358. me._createManipulatorBar();
  11359. me.moving = true;
  11360. me.start();
  11361. });
  11362. }
  11363. else {
  11364. alert(this.constants.labels["editError"]);
  11365. }
  11366. }
  11367. else {
  11368. alert(this.constants.labels["editBoundError"]);
  11369. }
  11370. },
  11371. /**
  11372. * delete everything in the selection
  11373. *
  11374. * @private
  11375. */
  11376. _deleteSelected : function() {
  11377. if (!this._selectionIsEmpty() && this.editMode == true) {
  11378. if (!this._clusterInSelection()) {
  11379. var selectedNodes = this.getSelectedNodes();
  11380. var selectedEdges = this.getSelectedEdges();
  11381. if (this.triggerFunctions.del) {
  11382. var me = this;
  11383. var data = {nodes: selectedNodes, edges: selectedEdges};
  11384. if (this.triggerFunctions.del.length = 2) {
  11385. this.triggerFunctions.del(data, function (finalizedData) {
  11386. me.edgesData.remove(finalizedData.edges);
  11387. me.nodesData.remove(finalizedData.nodes);
  11388. me._unselectAll();
  11389. me.moving = true;
  11390. me.start();
  11391. });
  11392. }
  11393. else {
  11394. alert(this.constants.labels["deleteError"])
  11395. }
  11396. }
  11397. else {
  11398. this.edgesData.remove(selectedEdges);
  11399. this.nodesData.remove(selectedNodes);
  11400. this._unselectAll();
  11401. this.moving = true;
  11402. this.start();
  11403. }
  11404. }
  11405. else {
  11406. alert(this.constants.labels["deleteClusterError"]);
  11407. }
  11408. }
  11409. }
  11410. };
  11411. /**
  11412. * Creation of the SectorMixin var.
  11413. *
  11414. * This contains all the functions the Graph object can use to employ the sector system.
  11415. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  11416. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  11417. *
  11418. * Alex de Mulder
  11419. * 21-01-2013
  11420. */
  11421. var SectorMixin = {
  11422. /**
  11423. * This function is only called by the setData function of the Graph object.
  11424. * This loads the global references into the active sector. This initializes the sector.
  11425. *
  11426. * @private
  11427. */
  11428. _putDataInSector : function() {
  11429. this.sectors["active"][this._sector()].nodes = this.nodes;
  11430. this.sectors["active"][this._sector()].edges = this.edges;
  11431. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  11432. },
  11433. /**
  11434. * /**
  11435. * This function sets the global references to nodes, edges and nodeIndices back to
  11436. * those of the supplied (active) sector. If a type is defined, do the specific type
  11437. *
  11438. * @param {String} sectorId
  11439. * @param {String} [sectorType] | "active" or "frozen"
  11440. * @private
  11441. */
  11442. _switchToSector : function(sectorId, sectorType) {
  11443. if (sectorType === undefined || sectorType == "active") {
  11444. this._switchToActiveSector(sectorId);
  11445. }
  11446. else {
  11447. this._switchToFrozenSector(sectorId);
  11448. }
  11449. },
  11450. /**
  11451. * This function sets the global references to nodes, edges and nodeIndices back to
  11452. * those of the supplied active sector.
  11453. *
  11454. * @param sectorId
  11455. * @private
  11456. */
  11457. _switchToActiveSector : function(sectorId) {
  11458. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  11459. this.nodes = this.sectors["active"][sectorId]["nodes"];
  11460. this.edges = this.sectors["active"][sectorId]["edges"];
  11461. },
  11462. /**
  11463. * This function sets the global references to nodes, edges and nodeIndices back to
  11464. * those of the supplied active sector.
  11465. *
  11466. * @param sectorId
  11467. * @private
  11468. */
  11469. _switchToSupportSector : function() {
  11470. this.nodeIndices = this.sectors["support"]["nodeIndices"];
  11471. this.nodes = this.sectors["support"]["nodes"];
  11472. this.edges = this.sectors["support"]["edges"];
  11473. },
  11474. /**
  11475. * This function sets the global references to nodes, edges and nodeIndices back to
  11476. * those of the supplied frozen sector.
  11477. *
  11478. * @param sectorId
  11479. * @private
  11480. */
  11481. _switchToFrozenSector : function(sectorId) {
  11482. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  11483. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  11484. this.edges = this.sectors["frozen"][sectorId]["edges"];
  11485. },
  11486. /**
  11487. * This function sets the global references to nodes, edges and nodeIndices back to
  11488. * those of the currently active sector.
  11489. *
  11490. * @private
  11491. */
  11492. _loadLatestSector : function() {
  11493. this._switchToSector(this._sector());
  11494. },
  11495. /**
  11496. * This function returns the currently active sector Id
  11497. *
  11498. * @returns {String}
  11499. * @private
  11500. */
  11501. _sector : function() {
  11502. return this.activeSector[this.activeSector.length-1];
  11503. },
  11504. /**
  11505. * This function returns the previously active sector Id
  11506. *
  11507. * @returns {String}
  11508. * @private
  11509. */
  11510. _previousSector : function() {
  11511. if (this.activeSector.length > 1) {
  11512. return this.activeSector[this.activeSector.length-2];
  11513. }
  11514. else {
  11515. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  11516. }
  11517. },
  11518. /**
  11519. * We add the active sector at the end of the this.activeSector array
  11520. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  11521. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  11522. *
  11523. * @param newId
  11524. * @private
  11525. */
  11526. _setActiveSector : function(newId) {
  11527. this.activeSector.push(newId);
  11528. },
  11529. /**
  11530. * We remove the currently active sector id from the active sector stack. This happens when
  11531. * we reactivate the previously active sector
  11532. *
  11533. * @private
  11534. */
  11535. _forgetLastSector : function() {
  11536. this.activeSector.pop();
  11537. },
  11538. /**
  11539. * This function creates a new active sector with the supplied newId. This newId
  11540. * is the expanding node id.
  11541. *
  11542. * @param {String} newId | Id of the new active sector
  11543. * @private
  11544. */
  11545. _createNewSector : function(newId) {
  11546. // create the new sector
  11547. this.sectors["active"][newId] = {"nodes":{},
  11548. "edges":{},
  11549. "nodeIndices":[],
  11550. "formationScale": this.scale,
  11551. "drawingNode": undefined};
  11552. // create the new sector render node. This gives visual feedback that you are in a new sector.
  11553. this.sectors["active"][newId]['drawingNode'] = new Node(
  11554. {id:newId,
  11555. color: {
  11556. background: "#eaefef",
  11557. border: "495c5e"
  11558. }
  11559. },{},{},this.constants);
  11560. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  11561. },
  11562. /**
  11563. * This function removes the currently active sector. This is called when we create a new
  11564. * active sector.
  11565. *
  11566. * @param {String} sectorId | Id of the active sector that will be removed
  11567. * @private
  11568. */
  11569. _deleteActiveSector : function(sectorId) {
  11570. delete this.sectors["active"][sectorId];
  11571. },
  11572. /**
  11573. * This function removes the currently active sector. This is called when we reactivate
  11574. * the previously active sector.
  11575. *
  11576. * @param {String} sectorId | Id of the active sector that will be removed
  11577. * @private
  11578. */
  11579. _deleteFrozenSector : function(sectorId) {
  11580. delete this.sectors["frozen"][sectorId];
  11581. },
  11582. /**
  11583. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  11584. * We copy the references, then delete the active entree.
  11585. *
  11586. * @param sectorId
  11587. * @private
  11588. */
  11589. _freezeSector : function(sectorId) {
  11590. // we move the set references from the active to the frozen stack.
  11591. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  11592. // we have moved the sector data into the frozen set, we now remove it from the active set
  11593. this._deleteActiveSector(sectorId);
  11594. },
  11595. /**
  11596. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  11597. * object to the "active" object.
  11598. *
  11599. * @param sectorId
  11600. * @private
  11601. */
  11602. _activateSector : function(sectorId) {
  11603. // we move the set references from the frozen to the active stack.
  11604. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  11605. // we have moved the sector data into the active set, we now remove it from the frozen stack
  11606. this._deleteFrozenSector(sectorId);
  11607. },
  11608. /**
  11609. * This function merges the data from the currently active sector with a frozen sector. This is used
  11610. * in the process of reverting back to the previously active sector.
  11611. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  11612. * upon the creation of a new active sector.
  11613. *
  11614. * @param sectorId
  11615. * @private
  11616. */
  11617. _mergeThisWithFrozen : function(sectorId) {
  11618. // copy all nodes
  11619. for (var nodeId in this.nodes) {
  11620. if (this.nodes.hasOwnProperty(nodeId)) {
  11621. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  11622. }
  11623. }
  11624. // copy all edges (if not fully clustered, else there are no edges)
  11625. for (var edgeId in this.edges) {
  11626. if (this.edges.hasOwnProperty(edgeId)) {
  11627. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  11628. }
  11629. }
  11630. // merge the nodeIndices
  11631. for (var i = 0; i < this.nodeIndices.length; i++) {
  11632. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  11633. }
  11634. },
  11635. /**
  11636. * This clusters the sector to one cluster. It was a single cluster before this process started so
  11637. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  11638. *
  11639. * @private
  11640. */
  11641. _collapseThisToSingleCluster : function() {
  11642. this.clusterToFit(1,false);
  11643. },
  11644. /**
  11645. * We create a new active sector from the node that we want to open.
  11646. *
  11647. * @param node
  11648. * @private
  11649. */
  11650. _addSector : function(node) {
  11651. // this is the currently active sector
  11652. var sector = this._sector();
  11653. // // this should allow me to select nodes from a frozen set.
  11654. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  11655. // console.log("the node is part of the active sector");
  11656. // }
  11657. // else {
  11658. // console.log("I dont know what the fuck happened!!");
  11659. // }
  11660. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  11661. delete this.nodes[node.id];
  11662. var unqiueIdentifier = util.randomUUID();
  11663. // we fully freeze the currently active sector
  11664. this._freezeSector(sector);
  11665. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  11666. this._createNewSector(unqiueIdentifier);
  11667. // we add the active sector to the sectors array to be able to revert these steps later on
  11668. this._setActiveSector(unqiueIdentifier);
  11669. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  11670. this._switchToSector(this._sector());
  11671. // finally we add the node we removed from our previous active sector to the new active sector
  11672. this.nodes[node.id] = node;
  11673. },
  11674. /**
  11675. * We close the sector that is currently open and revert back to the one before.
  11676. * If the active sector is the "default" sector, nothing happens.
  11677. *
  11678. * @private
  11679. */
  11680. _collapseSector : function() {
  11681. // the currently active sector
  11682. var sector = this._sector();
  11683. // we cannot collapse the default sector
  11684. if (sector != "default") {
  11685. if ((this.nodeIndices.length == 1) ||
  11686. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  11687. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  11688. var previousSector = this._previousSector();
  11689. // we collapse the sector back to a single cluster
  11690. this._collapseThisToSingleCluster();
  11691. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  11692. // This previous sector is the one we will reactivate
  11693. this._mergeThisWithFrozen(previousSector);
  11694. // the previously active (frozen) sector now has all the data from the currently active sector.
  11695. // we can now delete the active sector.
  11696. this._deleteActiveSector(sector);
  11697. // we activate the previously active (and currently frozen) sector.
  11698. this._activateSector(previousSector);
  11699. // we load the references from the newly active sector into the global references
  11700. this._switchToSector(previousSector);
  11701. // we forget the previously active sector because we reverted to the one before
  11702. this._forgetLastSector();
  11703. // finally, we update the node index list.
  11704. this._updateNodeIndexList();
  11705. // we refresh the list with calulation nodes and calculation node indices.
  11706. this._updateCalculationNodes();
  11707. }
  11708. }
  11709. },
  11710. /**
  11711. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11712. *
  11713. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11714. * | we dont pass the function itself because then the "this" is the window object
  11715. * | instead of the Graph object
  11716. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11717. * @private
  11718. */
  11719. _doInAllActiveSectors : function(runFunction,argument) {
  11720. if (argument === undefined) {
  11721. for (var sector in this.sectors["active"]) {
  11722. if (this.sectors["active"].hasOwnProperty(sector)) {
  11723. // switch the global references to those of this sector
  11724. this._switchToActiveSector(sector);
  11725. this[runFunction]();
  11726. }
  11727. }
  11728. }
  11729. else {
  11730. for (var sector in this.sectors["active"]) {
  11731. if (this.sectors["active"].hasOwnProperty(sector)) {
  11732. // switch the global references to those of this sector
  11733. this._switchToActiveSector(sector);
  11734. var args = Array.prototype.splice.call(arguments, 1);
  11735. if (args.length > 1) {
  11736. this[runFunction](args[0],args[1]);
  11737. }
  11738. else {
  11739. this[runFunction](argument);
  11740. }
  11741. }
  11742. }
  11743. }
  11744. // we revert the global references back to our active sector
  11745. this._loadLatestSector();
  11746. },
  11747. /**
  11748. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  11749. *
  11750. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11751. * | we dont pass the function itself because then the "this" is the window object
  11752. * | instead of the Graph object
  11753. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11754. * @private
  11755. */
  11756. _doInSupportSector : function(runFunction,argument) {
  11757. if (argument === undefined) {
  11758. this._switchToSupportSector();
  11759. this[runFunction]();
  11760. }
  11761. else {
  11762. this._switchToSupportSector();
  11763. var args = Array.prototype.splice.call(arguments, 1);
  11764. if (args.length > 1) {
  11765. this[runFunction](args[0],args[1]);
  11766. }
  11767. else {
  11768. this[runFunction](argument);
  11769. }
  11770. }
  11771. // we revert the global references back to our active sector
  11772. this._loadLatestSector();
  11773. },
  11774. /**
  11775. * This runs a function in all frozen sectors. This is used in the _redraw().
  11776. *
  11777. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11778. * | we don't pass the function itself because then the "this" is the window object
  11779. * | instead of the Graph object
  11780. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11781. * @private
  11782. */
  11783. _doInAllFrozenSectors : function(runFunction,argument) {
  11784. if (argument === undefined) {
  11785. for (var sector in this.sectors["frozen"]) {
  11786. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11787. // switch the global references to those of this sector
  11788. this._switchToFrozenSector(sector);
  11789. this[runFunction]();
  11790. }
  11791. }
  11792. }
  11793. else {
  11794. for (var sector in this.sectors["frozen"]) {
  11795. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11796. // switch the global references to those of this sector
  11797. this._switchToFrozenSector(sector);
  11798. var args = Array.prototype.splice.call(arguments, 1);
  11799. if (args.length > 1) {
  11800. this[runFunction](args[0],args[1]);
  11801. }
  11802. else {
  11803. this[runFunction](argument);
  11804. }
  11805. }
  11806. }
  11807. }
  11808. this._loadLatestSector();
  11809. },
  11810. /**
  11811. * This runs a function in all sectors. This is used in the _redraw().
  11812. *
  11813. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  11814. * | we don't pass the function itself because then the "this" is the window object
  11815. * | instead of the Graph object
  11816. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  11817. * @private
  11818. */
  11819. _doInAllSectors : function(runFunction,argument) {
  11820. var args = Array.prototype.splice.call(arguments, 1);
  11821. if (argument === undefined) {
  11822. this._doInAllActiveSectors(runFunction);
  11823. this._doInAllFrozenSectors(runFunction);
  11824. }
  11825. else {
  11826. if (args.length > 1) {
  11827. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  11828. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  11829. }
  11830. else {
  11831. this._doInAllActiveSectors(runFunction,argument);
  11832. this._doInAllFrozenSectors(runFunction,argument);
  11833. }
  11834. }
  11835. },
  11836. /**
  11837. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  11838. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  11839. *
  11840. * @private
  11841. */
  11842. _clearNodeIndexList : function() {
  11843. var sector = this._sector();
  11844. this.sectors["active"][sector]["nodeIndices"] = [];
  11845. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  11846. },
  11847. /**
  11848. * Draw the encompassing sector node
  11849. *
  11850. * @param ctx
  11851. * @param sectorType
  11852. * @private
  11853. */
  11854. _drawSectorNodes : function(ctx,sectorType) {
  11855. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  11856. for (var sector in this.sectors[sectorType]) {
  11857. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  11858. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  11859. this._switchToSector(sector,sectorType);
  11860. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  11861. for (var nodeId in this.nodes) {
  11862. if (this.nodes.hasOwnProperty(nodeId)) {
  11863. node = this.nodes[nodeId];
  11864. node.resize(ctx);
  11865. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  11866. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  11867. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  11868. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  11869. }
  11870. }
  11871. node = this.sectors[sectorType][sector]["drawingNode"];
  11872. node.x = 0.5 * (maxX + minX);
  11873. node.y = 0.5 * (maxY + minY);
  11874. node.width = 2 * (node.x - minX);
  11875. node.height = 2 * (node.y - minY);
  11876. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  11877. node.setScale(this.scale);
  11878. node._drawCircle(ctx);
  11879. }
  11880. }
  11881. }
  11882. },
  11883. _drawAllSectorNodes : function(ctx) {
  11884. this._drawSectorNodes(ctx,"frozen");
  11885. this._drawSectorNodes(ctx,"active");
  11886. this._loadLatestSector();
  11887. }
  11888. };
  11889. /**
  11890. * Creation of the ClusterMixin var.
  11891. *
  11892. * This contains all the functions the Graph object can use to employ clustering
  11893. *
  11894. * Alex de Mulder
  11895. * 21-01-2013
  11896. */
  11897. var ClusterMixin = {
  11898. /**
  11899. * This is only called in the constructor of the graph object
  11900. *
  11901. */
  11902. startWithClustering : function() {
  11903. // cluster if the data set is big
  11904. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  11905. // updates the lables after clustering
  11906. this.updateLabels();
  11907. // this is called here because if clusterin is disabled, the start and stabilize are called in
  11908. // the setData function.
  11909. if (this.stabilize) {
  11910. this._stabilize();
  11911. }
  11912. this.start();
  11913. },
  11914. /**
  11915. * This function clusters until the initialMaxNodes has been reached
  11916. *
  11917. * @param {Number} maxNumberOfNodes
  11918. * @param {Boolean} reposition
  11919. */
  11920. clusterToFit : function(maxNumberOfNodes, reposition) {
  11921. var numberOfNodes = this.nodeIndices.length;
  11922. var maxLevels = 50;
  11923. var level = 0;
  11924. // we first cluster the hubs, then we pull in the outliers, repeat
  11925. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  11926. if (level % 3 == 0) {
  11927. this.forceAggregateHubs(true);
  11928. this.normalizeClusterLevels();
  11929. }
  11930. else {
  11931. this.increaseClusterLevel(); // this also includes a cluster normalization
  11932. }
  11933. numberOfNodes = this.nodeIndices.length;
  11934. level += 1;
  11935. }
  11936. // after the clustering we reposition the nodes to reduce the initial chaos
  11937. if (level > 0 && reposition == true) {
  11938. this.repositionNodes();
  11939. }
  11940. this._updateCalculationNodes();
  11941. },
  11942. /**
  11943. * This function can be called to open up a specific cluster. It is only called by
  11944. * It will unpack the cluster back one level.
  11945. *
  11946. * @param node | Node object: cluster to open.
  11947. */
  11948. openCluster : function(node) {
  11949. var isMovingBeforeClustering = this.moving;
  11950. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  11951. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  11952. // this loads a new sector, loads the nodes and edges and nodeIndices of it.
  11953. this._addSector(node);
  11954. var level = 0;
  11955. // we decluster until we reach a decent number of nodes
  11956. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  11957. this.decreaseClusterLevel();
  11958. level += 1;
  11959. }
  11960. }
  11961. else {
  11962. this._expandClusterNode(node,false,true);
  11963. // update the index list, dynamic edges and labels
  11964. this._updateNodeIndexList();
  11965. this._updateDynamicEdges();
  11966. this._updateCalculationNodes();
  11967. this.updateLabels();
  11968. }
  11969. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  11970. if (this.moving != isMovingBeforeClustering) {
  11971. this.start();
  11972. }
  11973. },
  11974. /**
  11975. * This calls the updateClustes with default arguments
  11976. */
  11977. updateClustersDefault : function() {
  11978. if (this.constants.clustering.enabled == true) {
  11979. this.updateClusters(0,false,false);
  11980. }
  11981. },
  11982. /**
  11983. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  11984. * be clustered with their connected node. This can be repeated as many times as needed.
  11985. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  11986. */
  11987. increaseClusterLevel : function() {
  11988. this.updateClusters(-1,false,true);
  11989. },
  11990. /**
  11991. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  11992. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  11993. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  11994. */
  11995. decreaseClusterLevel : function() {
  11996. this.updateClusters(1,false,true);
  11997. },
  11998. /**
  11999. * This is the main clustering function. It clusters and declusters on zoom or forced
  12000. * This function clusters on zoom, it can be called with a predefined zoom direction
  12001. * If out, check if we can form clusters, if in, check if we can open clusters.
  12002. * This function is only called from _zoom()
  12003. *
  12004. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  12005. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  12006. * @param {Boolean} force | enabled or disable forcing
  12007. * @param {Boolean} doNotStart | if true do not call start
  12008. *
  12009. */
  12010. updateClusters : function(zoomDirection,recursive,force,doNotStart) {
  12011. var isMovingBeforeClustering = this.moving;
  12012. var amountOfNodes = this.nodeIndices.length;
  12013. // on zoom out collapse the sector if the scale is at the level the sector was made
  12014. if (this.previousScale > this.scale && zoomDirection == 0) {
  12015. this._collapseSector();
  12016. }
  12017. // check if we zoom in or out
  12018. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12019. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  12020. // outer nodes determines if it is being clustered
  12021. this._formClusters(force);
  12022. }
  12023. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  12024. if (force == true) {
  12025. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  12026. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  12027. this._openClusters(recursive,force);
  12028. }
  12029. else {
  12030. // if a cluster takes up a set percentage of the active window
  12031. this._openClustersBySize();
  12032. }
  12033. }
  12034. this._updateNodeIndexList();
  12035. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  12036. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  12037. this._aggregateHubs(force);
  12038. this._updateNodeIndexList();
  12039. }
  12040. // we now reduce chains.
  12041. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  12042. this.handleChains();
  12043. this._updateNodeIndexList();
  12044. }
  12045. this.previousScale = this.scale;
  12046. // rest of the update the index list, dynamic edges and labels
  12047. this._updateDynamicEdges();
  12048. this.updateLabels();
  12049. // if a cluster was formed, we increase the clusterSession
  12050. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  12051. this.clusterSession += 1;
  12052. // if clusters have been made, we normalize the cluster level
  12053. this.normalizeClusterLevels();
  12054. }
  12055. if (doNotStart == false || doNotStart === undefined) {
  12056. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12057. if (this.moving != isMovingBeforeClustering) {
  12058. this.start();
  12059. }
  12060. }
  12061. this._updateCalculationNodes();
  12062. },
  12063. /**
  12064. * This function handles the chains. It is called on every updateClusters().
  12065. */
  12066. handleChains : function() {
  12067. // after clustering we check how many chains there are
  12068. var chainPercentage = this._getChainFraction();
  12069. if (chainPercentage > this.constants.clustering.chainThreshold) {
  12070. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  12071. }
  12072. },
  12073. /**
  12074. * this functions starts clustering by hubs
  12075. * The minimum hub threshold is set globally
  12076. *
  12077. * @private
  12078. */
  12079. _aggregateHubs : function(force) {
  12080. this._getHubSize();
  12081. this._formClustersByHub(force,false);
  12082. },
  12083. /**
  12084. * This function is fired by keypress. It forces hubs to form.
  12085. *
  12086. */
  12087. forceAggregateHubs : function(doNotStart) {
  12088. var isMovingBeforeClustering = this.moving;
  12089. var amountOfNodes = this.nodeIndices.length;
  12090. this._aggregateHubs(true);
  12091. // update the index list, dynamic edges and labels
  12092. this._updateNodeIndexList();
  12093. this._updateDynamicEdges();
  12094. this.updateLabels();
  12095. // if a cluster was formed, we increase the clusterSession
  12096. if (this.nodeIndices.length != amountOfNodes) {
  12097. this.clusterSession += 1;
  12098. }
  12099. if (doNotStart == false || doNotStart === undefined) {
  12100. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  12101. if (this.moving != isMovingBeforeClustering) {
  12102. this.start();
  12103. }
  12104. }
  12105. },
  12106. /**
  12107. * If a cluster takes up more than a set percentage of the screen, open the cluster
  12108. *
  12109. * @private
  12110. */
  12111. _openClustersBySize : function() {
  12112. for (var nodeId in this.nodes) {
  12113. if (this.nodes.hasOwnProperty(nodeId)) {
  12114. var node = this.nodes[nodeId];
  12115. if (node.inView() == true) {
  12116. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  12117. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  12118. this.openCluster(node);
  12119. }
  12120. }
  12121. }
  12122. }
  12123. },
  12124. /**
  12125. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  12126. * has to be opened based on the current zoom level.
  12127. *
  12128. * @private
  12129. */
  12130. _openClusters : function(recursive,force) {
  12131. for (var i = 0; i < this.nodeIndices.length; i++) {
  12132. var node = this.nodes[this.nodeIndices[i]];
  12133. this._expandClusterNode(node,recursive,force);
  12134. this._updateCalculationNodes();
  12135. }
  12136. },
  12137. /**
  12138. * This function checks if a node has to be opened. This is done by checking the zoom level.
  12139. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  12140. * This recursive behaviour is optional and can be set by the recursive argument.
  12141. *
  12142. * @param {Node} parentNode | to check for cluster and expand
  12143. * @param {Boolean} recursive | enabled or disable recursive calling
  12144. * @param {Boolean} force | enabled or disable forcing
  12145. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  12146. * @private
  12147. */
  12148. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  12149. // first check if node is a cluster
  12150. if (parentNode.clusterSize > 1) {
  12151. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  12152. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  12153. openAll = true;
  12154. }
  12155. recursive = openAll ? true : recursive;
  12156. // if the last child has been added on a smaller scale than current scale decluster
  12157. if (parentNode.formationScale < this.scale || force == true) {
  12158. // we will check if any of the contained child nodes should be removed from the cluster
  12159. for (var containedNodeId in parentNode.containedNodes) {
  12160. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  12161. var childNode = parentNode.containedNodes[containedNodeId];
  12162. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  12163. // the largest cluster is the one that comes from outside
  12164. if (force == true) {
  12165. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  12166. || openAll) {
  12167. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12168. }
  12169. }
  12170. else {
  12171. if (this._nodeInActiveArea(parentNode)) {
  12172. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  12173. }
  12174. }
  12175. }
  12176. }
  12177. }
  12178. }
  12179. },
  12180. /**
  12181. * ONLY CALLED FROM _expandClusterNode
  12182. *
  12183. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  12184. * the child node from the parent contained_node object and put it back into the global nodes object.
  12185. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  12186. *
  12187. * @param {Node} parentNode | the parent node
  12188. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  12189. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  12190. * With force and recursive both true, the entire cluster is unpacked
  12191. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  12192. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  12193. * @private
  12194. */
  12195. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  12196. var childNode = parentNode.containedNodes[containedNodeId];
  12197. // if child node has been added on smaller scale than current, kick out
  12198. if (childNode.formationScale < this.scale || force == true) {
  12199. // unselect all selected items
  12200. this._unselectAll();
  12201. // put the child node back in the global nodes object
  12202. this.nodes[containedNodeId] = childNode;
  12203. // release the contained edges from this childNode back into the global edges
  12204. this._releaseContainedEdges(parentNode,childNode);
  12205. // reconnect rerouted edges to the childNode
  12206. this._connectEdgeBackToChild(parentNode,childNode);
  12207. // validate all edges in dynamicEdges
  12208. this._validateEdges(parentNode);
  12209. // undo the changes from the clustering operation on the parent node
  12210. parentNode.mass -= childNode.mass;
  12211. parentNode.clusterSize -= childNode.clusterSize;
  12212. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12213. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  12214. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  12215. childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
  12216. childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
  12217. // remove node from the list
  12218. delete parentNode.containedNodes[containedNodeId];
  12219. // check if there are other childs with this clusterSession in the parent.
  12220. var othersPresent = false;
  12221. for (var childNodeId in parentNode.containedNodes) {
  12222. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  12223. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  12224. othersPresent = true;
  12225. break;
  12226. }
  12227. }
  12228. }
  12229. // if there are no others, remove the cluster session from the list
  12230. if (othersPresent == false) {
  12231. parentNode.clusterSessions.pop();
  12232. }
  12233. this._repositionBezierNodes(childNode);
  12234. // this._repositionBezierNodes(parentNode);
  12235. // remove the clusterSession from the child node
  12236. childNode.clusterSession = 0;
  12237. // recalculate the size of the node on the next time the node is rendered
  12238. parentNode.clearSizeCache();
  12239. // restart the simulation to reorganise all nodes
  12240. this.moving = true;
  12241. }
  12242. // check if a further expansion step is possible if recursivity is enabled
  12243. if (recursive == true) {
  12244. this._expandClusterNode(childNode,recursive,force,openAll);
  12245. }
  12246. },
  12247. /**
  12248. * position the bezier nodes at the center of the edges
  12249. *
  12250. * @param node
  12251. * @private
  12252. */
  12253. _repositionBezierNodes : function(node) {
  12254. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12255. node.dynamicEdges[i].positionBezierNode();
  12256. }
  12257. },
  12258. /**
  12259. * This function checks if any nodes at the end of their trees have edges below a threshold length
  12260. * This function is called only from updateClusters()
  12261. * forceLevelCollapse ignores the length of the edge and collapses one level
  12262. * This means that a node with only one edge will be clustered with its connected node
  12263. *
  12264. * @private
  12265. * @param {Boolean} force
  12266. */
  12267. _formClusters : function(force) {
  12268. if (force == false) {
  12269. this._formClustersByZoom();
  12270. }
  12271. else {
  12272. this._forceClustersByZoom();
  12273. }
  12274. },
  12275. /**
  12276. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  12277. *
  12278. * @private
  12279. */
  12280. _formClustersByZoom : function() {
  12281. var dx,dy,length,
  12282. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12283. // check if any edges are shorter than minLength and start the clustering
  12284. // the clustering favours the node with the larger mass
  12285. for (var edgeId in this.edges) {
  12286. if (this.edges.hasOwnProperty(edgeId)) {
  12287. var edge = this.edges[edgeId];
  12288. if (edge.connected) {
  12289. if (edge.toId != edge.fromId) {
  12290. dx = (edge.to.x - edge.from.x);
  12291. dy = (edge.to.y - edge.from.y);
  12292. length = Math.sqrt(dx * dx + dy * dy);
  12293. if (length < minLength) {
  12294. // first check which node is larger
  12295. var parentNode = edge.from;
  12296. var childNode = edge.to;
  12297. if (edge.to.mass > edge.from.mass) {
  12298. parentNode = edge.to;
  12299. childNode = edge.from;
  12300. }
  12301. if (childNode.dynamicEdgesLength == 1) {
  12302. this._addToCluster(parentNode,childNode,false);
  12303. }
  12304. else if (parentNode.dynamicEdgesLength == 1) {
  12305. this._addToCluster(childNode,parentNode,false);
  12306. }
  12307. }
  12308. }
  12309. }
  12310. }
  12311. }
  12312. },
  12313. /**
  12314. * This function forces the graph to cluster all nodes with only one connecting edge to their
  12315. * connected node.
  12316. *
  12317. * @private
  12318. */
  12319. _forceClustersByZoom : function() {
  12320. for (var nodeId in this.nodes) {
  12321. // another node could have absorbed this child.
  12322. if (this.nodes.hasOwnProperty(nodeId)) {
  12323. var childNode = this.nodes[nodeId];
  12324. // the edges can be swallowed by another decrease
  12325. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  12326. var edge = childNode.dynamicEdges[0];
  12327. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  12328. // group to the largest node
  12329. if (childNode.id != parentNode.id) {
  12330. if (parentNode.mass > childNode.mass) {
  12331. this._addToCluster(parentNode,childNode,true);
  12332. }
  12333. else {
  12334. this._addToCluster(childNode,parentNode,true);
  12335. }
  12336. }
  12337. }
  12338. }
  12339. }
  12340. },
  12341. /**
  12342. * To keep the nodes of roughly equal size we normalize the cluster levels.
  12343. * This function clusters a node to its smallest connected neighbour.
  12344. *
  12345. * @param node
  12346. * @private
  12347. */
  12348. _clusterToSmallestNeighbour : function(node) {
  12349. var smallestNeighbour = -1;
  12350. var smallestNeighbourNode = null;
  12351. for (var i = 0; i < node.dynamicEdges.length; i++) {
  12352. if (node.dynamicEdges[i] !== undefined) {
  12353. var neighbour = null;
  12354. if (node.dynamicEdges[i].fromId != node.id) {
  12355. neighbour = node.dynamicEdges[i].from;
  12356. }
  12357. else if (node.dynamicEdges[i].toId != node.id) {
  12358. neighbour = node.dynamicEdges[i].to;
  12359. }
  12360. if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
  12361. smallestNeighbour = neighbour.clusterSessions.length;
  12362. smallestNeighbourNode = neighbour;
  12363. }
  12364. }
  12365. }
  12366. if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
  12367. this._addToCluster(neighbour, node, true);
  12368. }
  12369. },
  12370. /**
  12371. * This function forms clusters from hubs, it loops over all nodes
  12372. *
  12373. * @param {Boolean} force | Disregard zoom level
  12374. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12375. * @private
  12376. */
  12377. _formClustersByHub : function(force, onlyEqual) {
  12378. // we loop over all nodes in the list
  12379. for (var nodeId in this.nodes) {
  12380. // we check if it is still available since it can be used by the clustering in this loop
  12381. if (this.nodes.hasOwnProperty(nodeId)) {
  12382. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  12383. }
  12384. }
  12385. },
  12386. /**
  12387. * This function forms a cluster from a specific preselected hub node
  12388. *
  12389. * @param {Node} hubNode | the node we will cluster as a hub
  12390. * @param {Boolean} force | Disregard zoom level
  12391. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  12392. * @param {Number} [absorptionSizeOffset] |
  12393. * @private
  12394. */
  12395. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  12396. if (absorptionSizeOffset === undefined) {
  12397. absorptionSizeOffset = 0;
  12398. }
  12399. // we decide if the node is a hub
  12400. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  12401. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  12402. // initialize variables
  12403. var dx,dy,length;
  12404. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  12405. var allowCluster = false;
  12406. // we create a list of edges because the dynamicEdges change over the course of this loop
  12407. var edgesIdarray = [];
  12408. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  12409. for (var j = 0; j < amountOfInitialEdges; j++) {
  12410. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  12411. }
  12412. // if the hub clustering is not forces, we check if one of the edges connected
  12413. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  12414. if (force == false) {
  12415. allowCluster = false;
  12416. for (j = 0; j < amountOfInitialEdges; j++) {
  12417. var edge = this.edges[edgesIdarray[j]];
  12418. if (edge !== undefined) {
  12419. if (edge.connected) {
  12420. if (edge.toId != edge.fromId) {
  12421. dx = (edge.to.x - edge.from.x);
  12422. dy = (edge.to.y - edge.from.y);
  12423. length = Math.sqrt(dx * dx + dy * dy);
  12424. if (length < minLength) {
  12425. allowCluster = true;
  12426. break;
  12427. }
  12428. }
  12429. }
  12430. }
  12431. }
  12432. }
  12433. // start the clustering if allowed
  12434. if ((!force && allowCluster) || force) {
  12435. // we loop over all edges INITIALLY connected to this hub
  12436. for (j = 0; j < amountOfInitialEdges; j++) {
  12437. edge = this.edges[edgesIdarray[j]];
  12438. // the edge can be clustered by this function in a previous loop
  12439. if (edge !== undefined) {
  12440. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  12441. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  12442. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  12443. (childNode.id != hubNode.id)) {
  12444. this._addToCluster(hubNode,childNode,force);
  12445. }
  12446. }
  12447. }
  12448. }
  12449. }
  12450. },
  12451. /**
  12452. * This function adds the child node to the parent node, creating a cluster if it is not already.
  12453. *
  12454. * @param {Node} parentNode | this is the node that will house the child node
  12455. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  12456. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  12457. * @private
  12458. */
  12459. _addToCluster : function(parentNode, childNode, force) {
  12460. // join child node in the parent node
  12461. parentNode.containedNodes[childNode.id] = childNode;
  12462. // manage all the edges connected to the child and parent nodes
  12463. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  12464. var edge = childNode.dynamicEdges[i];
  12465. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  12466. this._addToContainedEdges(parentNode,childNode,edge);
  12467. }
  12468. else {
  12469. this._connectEdgeToCluster(parentNode,childNode,edge);
  12470. }
  12471. }
  12472. // a contained node has no dynamic edges.
  12473. childNode.dynamicEdges = [];
  12474. // remove circular edges from clusters
  12475. this._containCircularEdgesFromNode(parentNode,childNode);
  12476. // remove the childNode from the global nodes object
  12477. delete this.nodes[childNode.id];
  12478. // update the properties of the child and parent
  12479. var massBefore = parentNode.mass;
  12480. childNode.clusterSession = this.clusterSession;
  12481. parentNode.mass += childNode.mass;
  12482. parentNode.clusterSize += childNode.clusterSize;
  12483. parentNode.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
  12484. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  12485. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  12486. parentNode.clusterSessions.push(this.clusterSession);
  12487. }
  12488. // forced clusters only open from screen size and double tap
  12489. if (force == true) {
  12490. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  12491. parentNode.formationScale = 0;
  12492. }
  12493. else {
  12494. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  12495. }
  12496. // recalculate the size of the node on the next time the node is rendered
  12497. parentNode.clearSizeCache();
  12498. // set the pop-out scale for the childnode
  12499. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  12500. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  12501. childNode.clearVelocity();
  12502. // the mass has altered, preservation of energy dictates the velocity to be updated
  12503. parentNode.updateVelocity(massBefore);
  12504. // restart the simulation to reorganise all nodes
  12505. this.moving = true;
  12506. },
  12507. /**
  12508. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  12509. * This is a seperate function to allow for level-wise collapsing of the node barnesHutTree.
  12510. * It has to be called if a level is collapsed. It is called by _formClusters().
  12511. * @private
  12512. */
  12513. _updateDynamicEdges : function() {
  12514. for (var i = 0; i < this.nodeIndices.length; i++) {
  12515. var node = this.nodes[this.nodeIndices[i]];
  12516. node.dynamicEdgesLength = node.dynamicEdges.length;
  12517. // this corrects for multiple edges pointing at the same other node
  12518. var correction = 0;
  12519. if (node.dynamicEdgesLength > 1) {
  12520. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  12521. var edgeToId = node.dynamicEdges[j].toId;
  12522. var edgeFromId = node.dynamicEdges[j].fromId;
  12523. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  12524. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  12525. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  12526. correction += 1;
  12527. }
  12528. }
  12529. }
  12530. }
  12531. node.dynamicEdgesLength -= correction;
  12532. }
  12533. },
  12534. /**
  12535. * This adds an edge from the childNode to the contained edges of the parent node
  12536. *
  12537. * @param parentNode | Node object
  12538. * @param childNode | Node object
  12539. * @param edge | Edge object
  12540. * @private
  12541. */
  12542. _addToContainedEdges : function(parentNode, childNode, edge) {
  12543. // create an array object if it does not yet exist for this childNode
  12544. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  12545. parentNode.containedEdges[childNode.id] = []
  12546. }
  12547. // add this edge to the list
  12548. parentNode.containedEdges[childNode.id].push(edge);
  12549. // remove the edge from the global edges object
  12550. delete this.edges[edge.id];
  12551. // remove the edge from the parent object
  12552. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12553. if (parentNode.dynamicEdges[i].id == edge.id) {
  12554. parentNode.dynamicEdges.splice(i,1);
  12555. break;
  12556. }
  12557. }
  12558. },
  12559. /**
  12560. * This function connects an edge that was connected to a child node to the parent node.
  12561. * It keeps track of which nodes it has been connected to with the originalId array.
  12562. *
  12563. * @param {Node} parentNode | Node object
  12564. * @param {Node} childNode | Node object
  12565. * @param {Edge} edge | Edge object
  12566. * @private
  12567. */
  12568. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  12569. // handle circular edges
  12570. if (edge.toId == edge.fromId) {
  12571. this._addToContainedEdges(parentNode, childNode, edge);
  12572. }
  12573. else {
  12574. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  12575. edge.originalToId.push(childNode.id);
  12576. edge.to = parentNode;
  12577. edge.toId = parentNode.id;
  12578. }
  12579. else { // edge connected to other node with the "from" side
  12580. edge.originalFromId.push(childNode.id);
  12581. edge.from = parentNode;
  12582. edge.fromId = parentNode.id;
  12583. }
  12584. this._addToReroutedEdges(parentNode,childNode,edge);
  12585. }
  12586. },
  12587. /**
  12588. * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
  12589. * these edges inside of the cluster.
  12590. *
  12591. * @param parentNode
  12592. * @param childNode
  12593. * @private
  12594. */
  12595. _containCircularEdgesFromNode : function(parentNode, childNode) {
  12596. // manage all the edges connected to the child and parent nodes
  12597. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12598. var edge = parentNode.dynamicEdges[i];
  12599. // handle circular edges
  12600. if (edge.toId == edge.fromId) {
  12601. this._addToContainedEdges(parentNode, childNode, edge);
  12602. }
  12603. }
  12604. },
  12605. /**
  12606. * This adds an edge from the childNode to the rerouted edges of the parent node
  12607. *
  12608. * @param parentNode | Node object
  12609. * @param childNode | Node object
  12610. * @param edge | Edge object
  12611. * @private
  12612. */
  12613. _addToReroutedEdges : function(parentNode, childNode, edge) {
  12614. // create an array object if it does not yet exist for this childNode
  12615. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  12616. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  12617. parentNode.reroutedEdges[childNode.id] = [];
  12618. }
  12619. parentNode.reroutedEdges[childNode.id].push(edge);
  12620. // this edge becomes part of the dynamicEdges of the cluster node
  12621. parentNode.dynamicEdges.push(edge);
  12622. },
  12623. /**
  12624. * This function connects an edge that was connected to a cluster node back to the child node.
  12625. *
  12626. * @param parentNode | Node object
  12627. * @param childNode | Node object
  12628. * @private
  12629. */
  12630. _connectEdgeBackToChild : function(parentNode, childNode) {
  12631. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  12632. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  12633. var edge = parentNode.reroutedEdges[childNode.id][i];
  12634. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  12635. edge.originalFromId.pop();
  12636. edge.fromId = childNode.id;
  12637. edge.from = childNode;
  12638. }
  12639. else {
  12640. edge.originalToId.pop();
  12641. edge.toId = childNode.id;
  12642. edge.to = childNode;
  12643. }
  12644. // append this edge to the list of edges connecting to the childnode
  12645. childNode.dynamicEdges.push(edge);
  12646. // remove the edge from the parent object
  12647. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  12648. if (parentNode.dynamicEdges[j].id == edge.id) {
  12649. parentNode.dynamicEdges.splice(j,1);
  12650. break;
  12651. }
  12652. }
  12653. }
  12654. // remove the entry from the rerouted edges
  12655. delete parentNode.reroutedEdges[childNode.id];
  12656. }
  12657. },
  12658. /**
  12659. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  12660. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  12661. * parentNode
  12662. *
  12663. * @param parentNode | Node object
  12664. * @private
  12665. */
  12666. _validateEdges : function(parentNode) {
  12667. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  12668. var edge = parentNode.dynamicEdges[i];
  12669. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  12670. parentNode.dynamicEdges.splice(i,1);
  12671. }
  12672. }
  12673. },
  12674. /**
  12675. * This function released the contained edges back into the global domain and puts them back into the
  12676. * dynamic edges of both parent and child.
  12677. *
  12678. * @param {Node} parentNode |
  12679. * @param {Node} childNode |
  12680. * @private
  12681. */
  12682. _releaseContainedEdges : function(parentNode, childNode) {
  12683. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  12684. var edge = parentNode.containedEdges[childNode.id][i];
  12685. // put the edge back in the global edges object
  12686. this.edges[edge.id] = edge;
  12687. // put the edge back in the dynamic edges of the child and parent
  12688. childNode.dynamicEdges.push(edge);
  12689. parentNode.dynamicEdges.push(edge);
  12690. }
  12691. // remove the entry from the contained edges
  12692. delete parentNode.containedEdges[childNode.id];
  12693. },
  12694. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  12695. /**
  12696. * This updates the node labels for all nodes (for debugging purposes)
  12697. */
  12698. updateLabels : function() {
  12699. var nodeId;
  12700. // update node labels
  12701. for (nodeId in this.nodes) {
  12702. if (this.nodes.hasOwnProperty(nodeId)) {
  12703. var node = this.nodes[nodeId];
  12704. if (node.clusterSize > 1) {
  12705. node.label = "[".concat(String(node.clusterSize),"]");
  12706. }
  12707. }
  12708. }
  12709. // update node labels
  12710. for (nodeId in this.nodes) {
  12711. if (this.nodes.hasOwnProperty(nodeId)) {
  12712. node = this.nodes[nodeId];
  12713. if (node.clusterSize == 1) {
  12714. if (node.originalLabel !== undefined) {
  12715. node.label = node.originalLabel;
  12716. }
  12717. else {
  12718. node.label = String(node.id);
  12719. }
  12720. }
  12721. }
  12722. }
  12723. // /* Debug Override */
  12724. // for (nodeId in this.nodes) {
  12725. // if (this.nodes.hasOwnProperty(nodeId)) {
  12726. // node = this.nodes[nodeId];
  12727. // node.label = String(node.level);
  12728. // }
  12729. // }
  12730. },
  12731. /**
  12732. * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
  12733. * if the rest of the nodes are already a few cluster levels in.
  12734. * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
  12735. * clustered enough to the clusterToSmallestNeighbours function.
  12736. */
  12737. normalizeClusterLevels : function() {
  12738. var maxLevel = 0;
  12739. var minLevel = 1e9;
  12740. var clusterLevel = 0;
  12741. var nodeId;
  12742. // we loop over all nodes in the list
  12743. for (nodeId in this.nodes) {
  12744. if (this.nodes.hasOwnProperty(nodeId)) {
  12745. clusterLevel = this.nodes[nodeId].clusterSessions.length;
  12746. if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
  12747. if (minLevel > clusterLevel) {minLevel = clusterLevel;}
  12748. }
  12749. }
  12750. if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
  12751. var amountOfNodes = this.nodeIndices.length;
  12752. var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
  12753. // we loop over all nodes in the list
  12754. for (nodeId in this.nodes) {
  12755. if (this.nodes.hasOwnProperty(nodeId)) {
  12756. if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
  12757. this._clusterToSmallestNeighbour(this.nodes[nodeId]);
  12758. }
  12759. }
  12760. }
  12761. this._updateNodeIndexList();
  12762. this._updateDynamicEdges();
  12763. // if a cluster was formed, we increase the clusterSession
  12764. if (this.nodeIndices.length != amountOfNodes) {
  12765. this.clusterSession += 1;
  12766. }
  12767. }
  12768. },
  12769. /**
  12770. * This function determines if the cluster we want to decluster is in the active area
  12771. * this means around the zoom center
  12772. *
  12773. * @param {Node} node
  12774. * @returns {boolean}
  12775. * @private
  12776. */
  12777. _nodeInActiveArea : function(node) {
  12778. return (
  12779. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12780. &&
  12781. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  12782. )
  12783. },
  12784. /**
  12785. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  12786. * It puts large clusters away from the center and randomizes the order.
  12787. *
  12788. */
  12789. repositionNodes : function() {
  12790. for (var i = 0; i < this.nodeIndices.length; i++) {
  12791. var node = this.nodes[this.nodeIndices[i]];
  12792. if ((node.xFixed == false || node.yFixed == false)) {
  12793. var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.mass);
  12794. var angle = 2 * Math.PI * Math.random();
  12795. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  12796. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  12797. this._repositionBezierNodes(node);
  12798. }
  12799. }
  12800. },
  12801. /**
  12802. * We determine how many connections denote an important hub.
  12803. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  12804. *
  12805. * @private
  12806. */
  12807. _getHubSize : function() {
  12808. var average = 0;
  12809. var averageSquared = 0;
  12810. var hubCounter = 0;
  12811. var largestHub = 0;
  12812. for (var i = 0; i < this.nodeIndices.length; i++) {
  12813. var node = this.nodes[this.nodeIndices[i]];
  12814. if (node.dynamicEdgesLength > largestHub) {
  12815. largestHub = node.dynamicEdgesLength;
  12816. }
  12817. average += node.dynamicEdgesLength;
  12818. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  12819. hubCounter += 1;
  12820. }
  12821. average = average / hubCounter;
  12822. averageSquared = averageSquared / hubCounter;
  12823. var variance = averageSquared - Math.pow(average,2);
  12824. var standardDeviation = Math.sqrt(variance);
  12825. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  12826. // always have at least one to cluster
  12827. if (this.hubThreshold > largestHub) {
  12828. this.hubThreshold = largestHub;
  12829. }
  12830. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  12831. // console.log("hubThreshold:",this.hubThreshold);
  12832. },
  12833. /**
  12834. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12835. * with this amount we can cluster specifically on these chains.
  12836. *
  12837. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  12838. * @private
  12839. */
  12840. _reduceAmountOfChains : function(fraction) {
  12841. this.hubThreshold = 2;
  12842. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  12843. for (var nodeId in this.nodes) {
  12844. if (this.nodes.hasOwnProperty(nodeId)) {
  12845. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12846. if (reduceAmount > 0) {
  12847. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  12848. reduceAmount -= 1;
  12849. }
  12850. }
  12851. }
  12852. }
  12853. },
  12854. /**
  12855. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  12856. * with this amount we can cluster specifically on these chains.
  12857. *
  12858. * @private
  12859. */
  12860. _getChainFraction : function() {
  12861. var chains = 0;
  12862. var total = 0;
  12863. for (var nodeId in this.nodes) {
  12864. if (this.nodes.hasOwnProperty(nodeId)) {
  12865. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  12866. chains += 1;
  12867. }
  12868. total += 1;
  12869. }
  12870. }
  12871. return chains/total;
  12872. }
  12873. };
  12874. var SelectionMixin = {
  12875. /**
  12876. * This function can be called from the _doInAllSectors function
  12877. *
  12878. * @param object
  12879. * @param overlappingNodes
  12880. * @private
  12881. */
  12882. _getNodesOverlappingWith : function(object, overlappingNodes) {
  12883. var nodes = this.nodes;
  12884. for (var nodeId in nodes) {
  12885. if (nodes.hasOwnProperty(nodeId)) {
  12886. if (nodes[nodeId].isOverlappingWith(object)) {
  12887. overlappingNodes.push(nodeId);
  12888. }
  12889. }
  12890. }
  12891. },
  12892. /**
  12893. * retrieve all nodes overlapping with given object
  12894. * @param {Object} object An object with parameters left, top, right, bottom
  12895. * @return {Number[]} An array with id's of the overlapping nodes
  12896. * @private
  12897. */
  12898. _getAllNodesOverlappingWith : function (object) {
  12899. var overlappingNodes = [];
  12900. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  12901. return overlappingNodes;
  12902. },
  12903. /**
  12904. * Return a position object in canvasspace from a single point in screenspace
  12905. *
  12906. * @param pointer
  12907. * @returns {{left: number, top: number, right: number, bottom: number}}
  12908. * @private
  12909. */
  12910. _pointerToPositionObject : function(pointer) {
  12911. var x = this._XconvertDOMtoCanvas(pointer.x);
  12912. var y = this._YconvertDOMtoCanvas(pointer.y);
  12913. return {left: x,
  12914. top: y,
  12915. right: x,
  12916. bottom: y};
  12917. },
  12918. /**
  12919. * Get the top node at the a specific point (like a click)
  12920. *
  12921. * @param {{x: Number, y: Number}} pointer
  12922. * @return {Node | null} node
  12923. * @private
  12924. */
  12925. _getNodeAt : function (pointer) {
  12926. // we first check if this is an navigation controls element
  12927. var positionObject = this._pointerToPositionObject(pointer);
  12928. var overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  12929. // if there are overlapping nodes, select the last one, this is the
  12930. // one which is drawn on top of the others
  12931. if (overlappingNodes.length > 0) {
  12932. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  12933. }
  12934. else {
  12935. return null;
  12936. }
  12937. },
  12938. /**
  12939. * retrieve all edges overlapping with given object, selector is around center
  12940. * @param {Object} object An object with parameters left, top, right, bottom
  12941. * @return {Number[]} An array with id's of the overlapping nodes
  12942. * @private
  12943. */
  12944. _getEdgesOverlappingWith : function (object, overlappingEdges) {
  12945. var edges = this.edges;
  12946. for (var edgeId in edges) {
  12947. if (edges.hasOwnProperty(edgeId)) {
  12948. if (edges[edgeId].isOverlappingWith(object)) {
  12949. overlappingEdges.push(edgeId);
  12950. }
  12951. }
  12952. }
  12953. },
  12954. /**
  12955. * retrieve all nodes overlapping with given object
  12956. * @param {Object} object An object with parameters left, top, right, bottom
  12957. * @return {Number[]} An array with id's of the overlapping nodes
  12958. * @private
  12959. */
  12960. _getAllEdgesOverlappingWith : function (object) {
  12961. var overlappingEdges = [];
  12962. this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
  12963. return overlappingEdges;
  12964. },
  12965. /**
  12966. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  12967. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  12968. *
  12969. * @param pointer
  12970. * @returns {null}
  12971. * @private
  12972. */
  12973. _getEdgeAt : function(pointer) {
  12974. var positionObject = this._pointerToPositionObject(pointer);
  12975. var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject);
  12976. if (overlappingEdges.length > 0) {
  12977. return this.edges[overlappingEdges[overlappingEdges.length - 1]];
  12978. }
  12979. else {
  12980. return null;
  12981. }
  12982. },
  12983. /**
  12984. * Add object to the selection array.
  12985. *
  12986. * @param obj
  12987. * @private
  12988. */
  12989. _addToSelection : function(obj) {
  12990. if (obj instanceof Node) {
  12991. this.selectionObj.nodes[obj.id] = obj;
  12992. }
  12993. else {
  12994. this.selectionObj.edges[obj.id] = obj;
  12995. }
  12996. },
  12997. /**
  12998. * Add object to the selection array.
  12999. *
  13000. * @param obj
  13001. * @private
  13002. */
  13003. _addToHover : function(obj) {
  13004. if (obj instanceof Node) {
  13005. this.hoverObj.nodes[obj.id] = obj;
  13006. }
  13007. else {
  13008. this.hoverObj.edges[obj.id] = obj;
  13009. }
  13010. },
  13011. /**
  13012. * Remove a single option from selection.
  13013. *
  13014. * @param {Object} obj
  13015. * @private
  13016. */
  13017. _removeFromSelection : function(obj) {
  13018. if (obj instanceof Node) {
  13019. delete this.selectionObj.nodes[obj.id];
  13020. }
  13021. else {
  13022. delete this.selectionObj.edges[obj.id];
  13023. }
  13024. },
  13025. /**
  13026. * Unselect all. The selectionObj is useful for this.
  13027. *
  13028. * @param {Boolean} [doNotTrigger] | ignore trigger
  13029. * @private
  13030. */
  13031. _unselectAll : function(doNotTrigger) {
  13032. if (doNotTrigger === undefined) {
  13033. doNotTrigger = false;
  13034. }
  13035. for(var nodeId in this.selectionObj.nodes) {
  13036. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13037. this.selectionObj.nodes[nodeId].unselect();
  13038. }
  13039. }
  13040. for(var edgeId in this.selectionObj.edges) {
  13041. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13042. this.selectionObj.edges[edgeId].unselect();
  13043. }
  13044. }
  13045. this.selectionObj = {nodes:{},edges:{}};
  13046. if (doNotTrigger == false) {
  13047. this.emit('select', this.getSelection());
  13048. }
  13049. },
  13050. /**
  13051. * Unselect all clusters. The selectionObj is useful for this.
  13052. *
  13053. * @param {Boolean} [doNotTrigger] | ignore trigger
  13054. * @private
  13055. */
  13056. _unselectClusters : function(doNotTrigger) {
  13057. if (doNotTrigger === undefined) {
  13058. doNotTrigger = false;
  13059. }
  13060. for (var nodeId in this.selectionObj.nodes) {
  13061. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13062. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  13063. this.selectionObj.nodes[nodeId].unselect();
  13064. this._removeFromSelection(this.selectionObj.nodes[nodeId]);
  13065. }
  13066. }
  13067. }
  13068. if (doNotTrigger == false) {
  13069. this.emit('select', this.getSelection());
  13070. }
  13071. },
  13072. /**
  13073. * return the number of selected nodes
  13074. *
  13075. * @returns {number}
  13076. * @private
  13077. */
  13078. _getSelectedNodeCount : function() {
  13079. var count = 0;
  13080. for (var nodeId in this.selectionObj.nodes) {
  13081. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13082. count += 1;
  13083. }
  13084. }
  13085. return count;
  13086. },
  13087. /**
  13088. * return the number of selected nodes
  13089. *
  13090. * @returns {number}
  13091. * @private
  13092. */
  13093. _getSelectedNode : function() {
  13094. for (var nodeId in this.selectionObj.nodes) {
  13095. if (this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13096. return this.selectionObj.nodes[nodeId];
  13097. }
  13098. }
  13099. return null;
  13100. },
  13101. /**
  13102. * return the number of selected edges
  13103. *
  13104. * @returns {number}
  13105. * @private
  13106. */
  13107. _getSelectedEdgeCount : function() {
  13108. var count = 0;
  13109. for (var edgeId in this.selectionObj.edges) {
  13110. if (this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13111. count += 1;
  13112. }
  13113. }
  13114. return count;
  13115. },
  13116. /**
  13117. * return the number of selected objects.
  13118. *
  13119. * @returns {number}
  13120. * @private
  13121. */
  13122. _getSelectedObjectCount : function() {
  13123. var count = 0;
  13124. for(var nodeId in this.selectionObj.nodes) {
  13125. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13126. count += 1;
  13127. }
  13128. }
  13129. for(var edgeId in this.selectionObj.edges) {
  13130. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13131. count += 1;
  13132. }
  13133. }
  13134. return count;
  13135. },
  13136. /**
  13137. * Check if anything is selected
  13138. *
  13139. * @returns {boolean}
  13140. * @private
  13141. */
  13142. _selectionIsEmpty : function() {
  13143. for(var nodeId in this.selectionObj.nodes) {
  13144. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13145. return false;
  13146. }
  13147. }
  13148. for(var edgeId in this.selectionObj.edges) {
  13149. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13150. return false;
  13151. }
  13152. }
  13153. return true;
  13154. },
  13155. /**
  13156. * check if one of the selected nodes is a cluster.
  13157. *
  13158. * @returns {boolean}
  13159. * @private
  13160. */
  13161. _clusterInSelection : function() {
  13162. for(var nodeId in this.selectionObj.nodes) {
  13163. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13164. if (this.selectionObj.nodes[nodeId].clusterSize > 1) {
  13165. return true;
  13166. }
  13167. }
  13168. }
  13169. return false;
  13170. },
  13171. /**
  13172. * select the edges connected to the node that is being selected
  13173. *
  13174. * @param {Node} node
  13175. * @private
  13176. */
  13177. _selectConnectedEdges : function(node) {
  13178. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13179. var edge = node.dynamicEdges[i];
  13180. edge.select();
  13181. this._addToSelection(edge);
  13182. }
  13183. },
  13184. /**
  13185. * select the edges connected to the node that is being selected
  13186. *
  13187. * @param {Node} node
  13188. * @private
  13189. */
  13190. _hoverConnectedEdges : function(node) {
  13191. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13192. var edge = node.dynamicEdges[i];
  13193. edge.hover = true;
  13194. this._addToHover(edge);
  13195. }
  13196. },
  13197. /**
  13198. * unselect the edges connected to the node that is being selected
  13199. *
  13200. * @param {Node} node
  13201. * @private
  13202. */
  13203. _unselectConnectedEdges : function(node) {
  13204. for (var i = 0; i < node.dynamicEdges.length; i++) {
  13205. var edge = node.dynamicEdges[i];
  13206. edge.unselect();
  13207. this._removeFromSelection(edge);
  13208. }
  13209. },
  13210. /**
  13211. * This is called when someone clicks on a node. either select or deselect it.
  13212. * If there is an existing selection and we don't want to append to it, clear the existing selection
  13213. *
  13214. * @param {Node || Edge} object
  13215. * @param {Boolean} append
  13216. * @param {Boolean} [doNotTrigger] | ignore trigger
  13217. * @private
  13218. */
  13219. _selectObject : function(object, append, doNotTrigger) {
  13220. if (doNotTrigger === undefined) {
  13221. doNotTrigger = false;
  13222. }
  13223. if (this._selectionIsEmpty() == false && append == false && this.forceAppendSelection == false) {
  13224. this._unselectAll(true);
  13225. }
  13226. if (object.selected == false) {
  13227. object.select();
  13228. this._addToSelection(object);
  13229. if (object instanceof Node && this.blockConnectingEdgeSelection == false) {
  13230. this._selectConnectedEdges(object);
  13231. }
  13232. }
  13233. else {
  13234. object.unselect();
  13235. this._removeFromSelection(object);
  13236. }
  13237. if (doNotTrigger == false) {
  13238. this.emit('select', this.getSelection());
  13239. }
  13240. },
  13241. /**
  13242. * This is called when someone clicks on a node. either select or deselect it.
  13243. * If there is an existing selection and we don't want to append to it, clear the existing selection
  13244. *
  13245. * @param {Node || Edge} object
  13246. * @private
  13247. */
  13248. _blurObject : function(object) {
  13249. if (object.hover == true) {
  13250. object.hover = false;
  13251. this.emit("blurNode",{node:object.id});
  13252. }
  13253. },
  13254. /**
  13255. * This is called when someone clicks on a node. either select or deselect it.
  13256. * If there is an existing selection and we don't want to append to it, clear the existing selection
  13257. *
  13258. * @param {Node || Edge} object
  13259. * @private
  13260. */
  13261. _hoverObject : function(object) {
  13262. if (object.hover == false) {
  13263. object.hover = true;
  13264. this._addToHover(object);
  13265. if (object instanceof Node) {
  13266. this.emit("hoverNode",{node:object.id});
  13267. }
  13268. }
  13269. if (object instanceof Node) {
  13270. this._hoverConnectedEdges(object);
  13271. }
  13272. },
  13273. /**
  13274. * handles the selection part of the touch, only for navigation controls elements;
  13275. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  13276. * This is the most responsive solution
  13277. *
  13278. * @param {Object} pointer
  13279. * @private
  13280. */
  13281. _handleTouch : function(pointer) {
  13282. },
  13283. /**
  13284. * handles the selection part of the tap;
  13285. *
  13286. * @param {Object} pointer
  13287. * @private
  13288. */
  13289. _handleTap : function(pointer) {
  13290. var node = this._getNodeAt(pointer);
  13291. if (node != null) {
  13292. this._selectObject(node,false);
  13293. }
  13294. else {
  13295. var edge = this._getEdgeAt(pointer);
  13296. if (edge != null) {
  13297. this._selectObject(edge,false);
  13298. }
  13299. else {
  13300. this._unselectAll();
  13301. }
  13302. }
  13303. this.emit("click", this.getSelection());
  13304. this._redraw();
  13305. },
  13306. /**
  13307. * handles the selection part of the double tap and opens a cluster if needed
  13308. *
  13309. * @param {Object} pointer
  13310. * @private
  13311. */
  13312. _handleDoubleTap : function(pointer) {
  13313. var node = this._getNodeAt(pointer);
  13314. if (node != null && node !== undefined) {
  13315. // we reset the areaCenter here so the opening of the node will occur
  13316. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  13317. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  13318. this.openCluster(node);
  13319. }
  13320. this.emit("doubleClick", this.getSelection());
  13321. },
  13322. /**
  13323. * Handle the onHold selection part
  13324. *
  13325. * @param pointer
  13326. * @private
  13327. */
  13328. _handleOnHold : function(pointer) {
  13329. var node = this._getNodeAt(pointer);
  13330. if (node != null) {
  13331. this._selectObject(node,true);
  13332. }
  13333. else {
  13334. var edge = this._getEdgeAt(pointer);
  13335. if (edge != null) {
  13336. this._selectObject(edge,true);
  13337. }
  13338. }
  13339. this._redraw();
  13340. },
  13341. /**
  13342. * handle the onRelease event. These functions are here for the navigation controls module.
  13343. *
  13344. * @private
  13345. */
  13346. _handleOnRelease : function(pointer) {
  13347. },
  13348. /**
  13349. *
  13350. * retrieve the currently selected objects
  13351. * @return {Number[] | String[]} selection An array with the ids of the
  13352. * selected nodes.
  13353. */
  13354. getSelection : function() {
  13355. var nodeIds = this.getSelectedNodes();
  13356. var edgeIds = this.getSelectedEdges();
  13357. return {nodes:nodeIds, edges:edgeIds};
  13358. },
  13359. /**
  13360. *
  13361. * retrieve the currently selected nodes
  13362. * @return {String} selection An array with the ids of the
  13363. * selected nodes.
  13364. */
  13365. getSelectedNodes : function() {
  13366. var idArray = [];
  13367. for(var nodeId in this.selectionObj.nodes) {
  13368. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13369. idArray.push(nodeId);
  13370. }
  13371. }
  13372. return idArray
  13373. },
  13374. /**
  13375. *
  13376. * retrieve the currently selected edges
  13377. * @return {Array} selection An array with the ids of the
  13378. * selected nodes.
  13379. */
  13380. getSelectedEdges : function() {
  13381. var idArray = [];
  13382. for(var edgeId in this.selectionObj.edges) {
  13383. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13384. idArray.push(edgeId);
  13385. }
  13386. }
  13387. return idArray;
  13388. },
  13389. /**
  13390. * select zero or more nodes
  13391. * @param {Number[] | String[]} selection An array with the ids of the
  13392. * selected nodes.
  13393. */
  13394. setSelection : function(selection) {
  13395. var i, iMax, id;
  13396. if (!selection || (selection.length == undefined))
  13397. throw 'Selection must be an array with ids';
  13398. // first unselect any selected node
  13399. this._unselectAll(true);
  13400. for (i = 0, iMax = selection.length; i < iMax; i++) {
  13401. id = selection[i];
  13402. var node = this.nodes[id];
  13403. if (!node) {
  13404. throw new RangeError('Node with id "' + id + '" not found');
  13405. }
  13406. this._selectObject(node,true,true);
  13407. }
  13408. this.redraw();
  13409. },
  13410. /**
  13411. * Validate the selection: remove ids of nodes which no longer exist
  13412. * @private
  13413. */
  13414. _updateSelection : function () {
  13415. for(var nodeId in this.selectionObj.nodes) {
  13416. if(this.selectionObj.nodes.hasOwnProperty(nodeId)) {
  13417. if (!this.nodes.hasOwnProperty(nodeId)) {
  13418. delete this.selectionObj.nodes[nodeId];
  13419. }
  13420. }
  13421. }
  13422. for(var edgeId in this.selectionObj.edges) {
  13423. if(this.selectionObj.edges.hasOwnProperty(edgeId)) {
  13424. if (!this.edges.hasOwnProperty(edgeId)) {
  13425. delete this.selectionObj.edges[edgeId];
  13426. }
  13427. }
  13428. }
  13429. }
  13430. };
  13431. /**
  13432. * Created by Alex on 1/22/14.
  13433. */
  13434. var NavigationMixin = {
  13435. _cleanNavigation : function() {
  13436. // clean up previosu navigation items
  13437. var wrapper = document.getElementById('graph-navigation_wrapper');
  13438. if (wrapper != null) {
  13439. this.containerElement.removeChild(wrapper);
  13440. }
  13441. document.onmouseup = null;
  13442. },
  13443. /**
  13444. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  13445. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  13446. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  13447. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  13448. *
  13449. * @private
  13450. */
  13451. _loadNavigationElements : function() {
  13452. this._cleanNavigation();
  13453. this.navigationDivs = {};
  13454. var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
  13455. var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','zoomExtent'];
  13456. this.navigationDivs['wrapper'] = document.createElement('div');
  13457. this.navigationDivs['wrapper'].id = "graph-navigation_wrapper";
  13458. this.navigationDivs['wrapper'].style.position = "absolute";
  13459. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  13460. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  13461. this.containerElement.insertBefore(this.navigationDivs['wrapper'],this.frame);
  13462. for (var i = 0; i < navigationDivs.length; i++) {
  13463. this.navigationDivs[navigationDivs[i]] = document.createElement('div');
  13464. this.navigationDivs[navigationDivs[i]].id = "graph-navigation_" + navigationDivs[i];
  13465. this.navigationDivs[navigationDivs[i]].className = "graph-navigation " + navigationDivs[i];
  13466. this.navigationDivs['wrapper'].appendChild(this.navigationDivs[navigationDivs[i]]);
  13467. this.navigationDivs[navigationDivs[i]].onmousedown = this[navigationDivActions[i]].bind(this);
  13468. }
  13469. document.onmouseup = this._stopMovement.bind(this);
  13470. },
  13471. /**
  13472. * this stops all movement induced by the navigation buttons
  13473. *
  13474. * @private
  13475. */
  13476. _stopMovement : function() {
  13477. this._xStopMoving();
  13478. this._yStopMoving();
  13479. this._stopZoom();
  13480. },
  13481. /**
  13482. * stops the actions performed by page up and down etc.
  13483. *
  13484. * @param event
  13485. * @private
  13486. */
  13487. _preventDefault : function(event) {
  13488. if (event !== undefined) {
  13489. if (event.preventDefault) {
  13490. event.preventDefault();
  13491. } else {
  13492. event.returnValue = false;
  13493. }
  13494. }
  13495. },
  13496. /**
  13497. * move the screen up
  13498. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  13499. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  13500. * To avoid this behaviour, we do the translation in the start loop.
  13501. *
  13502. * @private
  13503. */
  13504. _moveUp : function(event) {
  13505. this.yIncrement = this.constants.keyboard.speed.y;
  13506. this.start(); // if there is no node movement, the calculation wont be done
  13507. this._preventDefault(event);
  13508. if (this.navigationDivs) {
  13509. this.navigationDivs['up'].className += " active";
  13510. }
  13511. },
  13512. /**
  13513. * move the screen down
  13514. * @private
  13515. */
  13516. _moveDown : function(event) {
  13517. this.yIncrement = -this.constants.keyboard.speed.y;
  13518. this.start(); // if there is no node movement, the calculation wont be done
  13519. this._preventDefault(event);
  13520. if (this.navigationDivs) {
  13521. this.navigationDivs['down'].className += " active";
  13522. }
  13523. },
  13524. /**
  13525. * move the screen left
  13526. * @private
  13527. */
  13528. _moveLeft : function(event) {
  13529. this.xIncrement = this.constants.keyboard.speed.x;
  13530. this.start(); // if there is no node movement, the calculation wont be done
  13531. this._preventDefault(event);
  13532. if (this.navigationDivs) {
  13533. this.navigationDivs['left'].className += " active";
  13534. }
  13535. },
  13536. /**
  13537. * move the screen right
  13538. * @private
  13539. */
  13540. _moveRight : function(event) {
  13541. this.xIncrement = -this.constants.keyboard.speed.y;
  13542. this.start(); // if there is no node movement, the calculation wont be done
  13543. this._preventDefault(event);
  13544. if (this.navigationDivs) {
  13545. this.navigationDivs['right'].className += " active";
  13546. }
  13547. },
  13548. /**
  13549. * Zoom in, using the same method as the movement.
  13550. * @private
  13551. */
  13552. _zoomIn : function(event) {
  13553. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  13554. this.start(); // if there is no node movement, the calculation wont be done
  13555. this._preventDefault(event);
  13556. if (this.navigationDivs) {
  13557. this.navigationDivs['zoomIn'].className += " active";
  13558. }
  13559. },
  13560. /**
  13561. * Zoom out
  13562. * @private
  13563. */
  13564. _zoomOut : function() {
  13565. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  13566. this.start(); // if there is no node movement, the calculation wont be done
  13567. this._preventDefault(event);
  13568. if (this.navigationDivs) {
  13569. this.navigationDivs['zoomOut'].className += " active";
  13570. }
  13571. },
  13572. /**
  13573. * Stop zooming and unhighlight the zoom controls
  13574. * @private
  13575. */
  13576. _stopZoom : function() {
  13577. this.zoomIncrement = 0;
  13578. if (this.navigationDivs) {
  13579. this.navigationDivs['zoomIn'].className = this.navigationDivs['zoomIn'].className.replace(" active","");
  13580. this.navigationDivs['zoomOut'].className = this.navigationDivs['zoomOut'].className.replace(" active","");
  13581. }
  13582. },
  13583. /**
  13584. * Stop moving in the Y direction and unHighlight the up and down
  13585. * @private
  13586. */
  13587. _yStopMoving : function() {
  13588. this.yIncrement = 0;
  13589. if (this.navigationDivs) {
  13590. this.navigationDivs['up'].className = this.navigationDivs['up'].className.replace(" active","");
  13591. this.navigationDivs['down'].className = this.navigationDivs['down'].className.replace(" active","");
  13592. }
  13593. },
  13594. /**
  13595. * Stop moving in the X direction and unHighlight left and right.
  13596. * @private
  13597. */
  13598. _xStopMoving : function() {
  13599. this.xIncrement = 0;
  13600. if (this.navigationDivs) {
  13601. this.navigationDivs['left'].className = this.navigationDivs['left'].className.replace(" active","");
  13602. this.navigationDivs['right'].className = this.navigationDivs['right'].className.replace(" active","");
  13603. }
  13604. }
  13605. };
  13606. /**
  13607. * Created by Alex on 2/10/14.
  13608. */
  13609. var graphMixinLoaders = {
  13610. /**
  13611. * Load a mixin into the graph object
  13612. *
  13613. * @param {Object} sourceVariable | this object has to contain functions.
  13614. * @private
  13615. */
  13616. _loadMixin: function (sourceVariable) {
  13617. for (var mixinFunction in sourceVariable) {
  13618. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13619. Graph.prototype[mixinFunction] = sourceVariable[mixinFunction];
  13620. }
  13621. }
  13622. },
  13623. /**
  13624. * removes a mixin from the graph object.
  13625. *
  13626. * @param {Object} sourceVariable | this object has to contain functions.
  13627. * @private
  13628. */
  13629. _clearMixin: function (sourceVariable) {
  13630. for (var mixinFunction in sourceVariable) {
  13631. if (sourceVariable.hasOwnProperty(mixinFunction)) {
  13632. Graph.prototype[mixinFunction] = undefined;
  13633. }
  13634. }
  13635. },
  13636. /**
  13637. * Mixin the physics system and initialize the parameters required.
  13638. *
  13639. * @private
  13640. */
  13641. _loadPhysicsSystem: function () {
  13642. this._loadMixin(physicsMixin);
  13643. this._loadSelectedForceSolver();
  13644. if (this.constants.configurePhysics == true) {
  13645. this._loadPhysicsConfiguration();
  13646. }
  13647. },
  13648. /**
  13649. * Mixin the cluster system and initialize the parameters required.
  13650. *
  13651. * @private
  13652. */
  13653. _loadClusterSystem: function () {
  13654. this.clusterSession = 0;
  13655. this.hubThreshold = 5;
  13656. this._loadMixin(ClusterMixin);
  13657. },
  13658. /**
  13659. * Mixin the sector system and initialize the parameters required
  13660. *
  13661. * @private
  13662. */
  13663. _loadSectorSystem: function () {
  13664. this.sectors = {};
  13665. this.activeSector = ["default"];
  13666. this.sectors["active"] = {};
  13667. this.sectors["active"]["default"] = {"nodes": {},
  13668. "edges": {},
  13669. "nodeIndices": [],
  13670. "formationScale": 1.0,
  13671. "drawingNode": undefined };
  13672. this.sectors["frozen"] = {};
  13673. this.sectors["support"] = {"nodes": {},
  13674. "edges": {},
  13675. "nodeIndices": [],
  13676. "formationScale": 1.0,
  13677. "drawingNode": undefined };
  13678. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  13679. this._loadMixin(SectorMixin);
  13680. },
  13681. /**
  13682. * Mixin the selection system and initialize the parameters required
  13683. *
  13684. * @private
  13685. */
  13686. _loadSelectionSystem: function () {
  13687. this.selectionObj = {nodes: {}, edges: {}};
  13688. this._loadMixin(SelectionMixin);
  13689. },
  13690. /**
  13691. * Mixin the navigationUI (User Interface) system and initialize the parameters required
  13692. *
  13693. * @private
  13694. */
  13695. _loadManipulationSystem: function () {
  13696. // reset global variables -- these are used by the selection of nodes and edges.
  13697. this.blockConnectingEdgeSelection = false;
  13698. this.forceAppendSelection = false;
  13699. if (this.constants.dataManipulation.enabled == true) {
  13700. // load the manipulator HTML elements. All styling done in css.
  13701. if (this.manipulationDiv === undefined) {
  13702. this.manipulationDiv = document.createElement('div');
  13703. this.manipulationDiv.className = 'graph-manipulationDiv';
  13704. this.manipulationDiv.id = 'graph-manipulationDiv';
  13705. if (this.editMode == true) {
  13706. this.manipulationDiv.style.display = "block";
  13707. }
  13708. else {
  13709. this.manipulationDiv.style.display = "none";
  13710. }
  13711. this.containerElement.insertBefore(this.manipulationDiv, this.frame);
  13712. }
  13713. if (this.editModeDiv === undefined) {
  13714. this.editModeDiv = document.createElement('div');
  13715. this.editModeDiv.className = 'graph-manipulation-editMode';
  13716. this.editModeDiv.id = 'graph-manipulation-editMode';
  13717. if (this.editMode == true) {
  13718. this.editModeDiv.style.display = "none";
  13719. }
  13720. else {
  13721. this.editModeDiv.style.display = "block";
  13722. }
  13723. this.containerElement.insertBefore(this.editModeDiv, this.frame);
  13724. }
  13725. if (this.closeDiv === undefined) {
  13726. this.closeDiv = document.createElement('div');
  13727. this.closeDiv.className = 'graph-manipulation-closeDiv';
  13728. this.closeDiv.id = 'graph-manipulation-closeDiv';
  13729. this.closeDiv.style.display = this.manipulationDiv.style.display;
  13730. this.containerElement.insertBefore(this.closeDiv, this.frame);
  13731. }
  13732. // load the manipulation functions
  13733. this._loadMixin(manipulationMixin);
  13734. // create the manipulator toolbar
  13735. this._createManipulatorBar();
  13736. }
  13737. else {
  13738. if (this.manipulationDiv !== undefined) {
  13739. // removes all the bindings and overloads
  13740. this._createManipulatorBar();
  13741. // remove the manipulation divs
  13742. this.containerElement.removeChild(this.manipulationDiv);
  13743. this.containerElement.removeChild(this.editModeDiv);
  13744. this.containerElement.removeChild(this.closeDiv);
  13745. this.manipulationDiv = undefined;
  13746. this.editModeDiv = undefined;
  13747. this.closeDiv = undefined;
  13748. // remove the mixin functions
  13749. this._clearMixin(manipulationMixin);
  13750. }
  13751. }
  13752. },
  13753. /**
  13754. * Mixin the navigation (User Interface) system and initialize the parameters required
  13755. *
  13756. * @private
  13757. */
  13758. _loadNavigationControls: function () {
  13759. this._loadMixin(NavigationMixin);
  13760. // the clean function removes the button divs, this is done to remove the bindings.
  13761. this._cleanNavigation();
  13762. if (this.constants.navigation.enabled == true) {
  13763. this._loadNavigationElements();
  13764. }
  13765. },
  13766. /**
  13767. * Mixin the hierarchical layout system.
  13768. *
  13769. * @private
  13770. */
  13771. _loadHierarchySystem: function () {
  13772. this._loadMixin(HierarchicalLayoutMixin);
  13773. }
  13774. };
  13775. /**
  13776. * @constructor Graph
  13777. * Create a graph visualization, displaying nodes and edges.
  13778. *
  13779. * @param {Element} container The DOM element in which the Graph will
  13780. * be created. Normally a div element.
  13781. * @param {Object} data An object containing parameters
  13782. * {Array} nodes
  13783. * {Array} edges
  13784. * @param {Object} options Options
  13785. */
  13786. function Graph (container, data, options) {
  13787. this._initializeMixinLoaders();
  13788. // create variables and set default values
  13789. this.containerElement = container;
  13790. this.width = '100%';
  13791. this.height = '100%';
  13792. // render and calculation settings
  13793. this.renderRefreshRate = 60; // hz (fps)
  13794. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  13795. this.renderTime = 0.5 * this.renderTimestep; // measured time it takes to render a frame
  13796. this.maxPhysicsTicksPerRender = 3; // max amount of physics ticks per render step.
  13797. this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
  13798. this.stabilize = true; // stabilize before displaying the graph
  13799. this.selectable = true;
  13800. this.initializing = true;
  13801. // these functions are triggered when the dataset is edited
  13802. this.triggerFunctions = {add:null,edit:null,connect:null,del:null};
  13803. // set constant values
  13804. this.constants = {
  13805. nodes: {
  13806. radiusMin: 5,
  13807. radiusMax: 20,
  13808. radius: 5,
  13809. shape: 'ellipse',
  13810. image: undefined,
  13811. widthMin: 16, // px
  13812. widthMax: 64, // px
  13813. fixed: false,
  13814. fontColor: 'black',
  13815. fontSize: 14, // px
  13816. fontFace: 'verdana',
  13817. level: -1,
  13818. color: {
  13819. border: '#2B7CE9',
  13820. background: '#97C2FC',
  13821. highlight: {
  13822. border: '#2B7CE9',
  13823. background: '#D2E5FF'
  13824. },
  13825. hover: {
  13826. border: '#2B7CE9',
  13827. background: '#D2E5FF'
  13828. }
  13829. },
  13830. borderColor: '#2B7CE9',
  13831. backgroundColor: '#97C2FC',
  13832. highlightColor: '#D2E5FF',
  13833. group: undefined
  13834. },
  13835. edges: {
  13836. widthMin: 1,
  13837. widthMax: 15,
  13838. width: 1,
  13839. hoverWidth: 1.5,
  13840. style: 'line',
  13841. color: {
  13842. color:'#848484',
  13843. highlight:'#848484',
  13844. hover: '#848484'
  13845. },
  13846. fontColor: '#343434',
  13847. fontSize: 14, // px
  13848. fontFace: 'arial',
  13849. fontFill: 'white',
  13850. arrowScaleFactor: 1,
  13851. dash: {
  13852. length: 10,
  13853. gap: 5,
  13854. altLength: undefined
  13855. }
  13856. },
  13857. configurePhysics:false,
  13858. physics: {
  13859. barnesHut: {
  13860. enabled: true,
  13861. theta: 1 / 0.6, // inverted to save time during calculation
  13862. gravitationalConstant: -2000,
  13863. centralGravity: 0.3,
  13864. springLength: 95,
  13865. springConstant: 0.04,
  13866. damping: 0.09
  13867. },
  13868. repulsion: {
  13869. centralGravity: 0.1,
  13870. springLength: 200,
  13871. springConstant: 0.05,
  13872. nodeDistance: 100,
  13873. damping: 0.09
  13874. },
  13875. hierarchicalRepulsion: {
  13876. enabled: false,
  13877. centralGravity: 0.0,
  13878. springLength: 100,
  13879. springConstant: 0.01,
  13880. nodeDistance: 60,
  13881. damping: 0.09
  13882. },
  13883. damping: null,
  13884. centralGravity: null,
  13885. springLength: null,
  13886. springConstant: null
  13887. },
  13888. clustering: { // Per Node in Cluster = PNiC
  13889. enabled: false, // (Boolean) | global on/off switch for clustering.
  13890. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  13891. 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
  13892. 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
  13893. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  13894. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  13895. sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  13896. 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.
  13897. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  13898. maxFontSize: 1000,
  13899. forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  13900. distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  13901. edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  13902. nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
  13903. height: 1, // (px PNiC) | growth of the height per node in cluster.
  13904. radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
  13905. maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
  13906. activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
  13907. clusterLevelDifference: 2
  13908. },
  13909. navigation: {
  13910. enabled: false
  13911. },
  13912. keyboard: {
  13913. enabled: false,
  13914. speed: {x: 10, y: 10, zoom: 0.02}
  13915. },
  13916. dataManipulation: {
  13917. enabled: false,
  13918. initiallyVisible: false
  13919. },
  13920. hierarchicalLayout: {
  13921. enabled:false,
  13922. levelSeparation: 150,
  13923. nodeSpacing: 100,
  13924. direction: "UD" // UD, DU, LR, RL
  13925. },
  13926. freezeForStabilization: false,
  13927. smoothCurves: true,
  13928. maxVelocity: 10,
  13929. minVelocity: 0.1, // px/s
  13930. stabilizationIterations: 1000, // maximum number of iteration to stabilize
  13931. labels:{
  13932. add:"Add Node",
  13933. edit:"Edit",
  13934. link:"Add Link",
  13935. del:"Delete selected",
  13936. editNode:"Edit Node",
  13937. back:"Back",
  13938. addDescription:"Click in an empty space to place a new node.",
  13939. linkDescription:"Click on a node and drag the edge to another node to connect them.",
  13940. addError:"The function for add does not support two arguments (data,callback).",
  13941. linkError:"The function for connect does not support two arguments (data,callback).",
  13942. editError:"The function for edit does not support two arguments (data, callback).",
  13943. editBoundError:"No edit function has been bound to this button.",
  13944. deleteError:"The function for delete does not support two arguments (data, callback).",
  13945. deleteClusterError:"Clusters cannot be deleted."
  13946. },
  13947. tooltip: {
  13948. delay: 300,
  13949. fontColor: 'black',
  13950. fontSize: 14, // px
  13951. fontFace: 'verdana',
  13952. color: {
  13953. border: '#666',
  13954. background: '#FFFFC6'
  13955. }
  13956. },
  13957. dragGraph: true,
  13958. dragNodes: true,
  13959. zoomable: true,
  13960. hover: false
  13961. };
  13962. this.hoverObj = {nodes:{},edges:{}};
  13963. // Node variables
  13964. var graph = this;
  13965. this.groups = new Groups(); // object with groups
  13966. this.images = new Images(); // object with images
  13967. this.images.setOnloadCallback(function () {
  13968. graph._redraw();
  13969. });
  13970. // keyboard navigation variables
  13971. this.xIncrement = 0;
  13972. this.yIncrement = 0;
  13973. this.zoomIncrement = 0;
  13974. // loading all the mixins:
  13975. // load the force calculation functions, grouped under the physics system.
  13976. this._loadPhysicsSystem();
  13977. // create a frame and canvas
  13978. this._create();
  13979. // load the sector system. (mandatory, fully integrated with Graph)
  13980. this._loadSectorSystem();
  13981. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  13982. this._loadClusterSystem();
  13983. // load the selection system. (mandatory, required by Graph)
  13984. this._loadSelectionSystem();
  13985. // load the selection system. (mandatory, required by Graph)
  13986. this._loadHierarchySystem();
  13987. // apply options
  13988. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  13989. this._setScale(1);
  13990. this.setOptions(options);
  13991. // other vars
  13992. this.freezeSimulation = false;// freeze the simulation
  13993. this.cachedFunctions = {};
  13994. // containers for nodes and edges
  13995. this.calculationNodes = {};
  13996. this.calculationNodeIndices = [];
  13997. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  13998. this.nodes = {}; // object with Node objects
  13999. this.edges = {}; // object with Edge objects
  14000. // position and scale variables and objects
  14001. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  14002. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14003. this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  14004. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  14005. this.scale = 1; // defining the global scale variable in the constructor
  14006. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  14007. // datasets or dataviews
  14008. this.nodesData = null; // A DataSet or DataView
  14009. this.edgesData = null; // A DataSet or DataView
  14010. // create event listeners used to subscribe on the DataSets of the nodes and edges
  14011. this.nodesListeners = {
  14012. 'add': function (event, params) {
  14013. graph._addNodes(params.items);
  14014. graph.start();
  14015. },
  14016. 'update': function (event, params) {
  14017. graph._updateNodes(params.items);
  14018. graph.start();
  14019. },
  14020. 'remove': function (event, params) {
  14021. graph._removeNodes(params.items);
  14022. graph.start();
  14023. }
  14024. };
  14025. this.edgesListeners = {
  14026. 'add': function (event, params) {
  14027. graph._addEdges(params.items);
  14028. graph.start();
  14029. },
  14030. 'update': function (event, params) {
  14031. graph._updateEdges(params.items);
  14032. graph.start();
  14033. },
  14034. 'remove': function (event, params) {
  14035. graph._removeEdges(params.items);
  14036. graph.start();
  14037. }
  14038. };
  14039. // properties for the animation
  14040. this.moving = true;
  14041. this.timer = undefined; // Scheduling function. Is definded in this.start();
  14042. // load data (the disable start variable will be the same as the enabled clustering)
  14043. this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
  14044. // hierarchical layout
  14045. this.initializing = false;
  14046. if (this.constants.hierarchicalLayout.enabled == true) {
  14047. this._setupHierarchicalLayout();
  14048. }
  14049. else {
  14050. // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
  14051. if (this.stabilize == false) {
  14052. this.zoomExtent(true,this.constants.clustering.enabled);
  14053. }
  14054. }
  14055. // if clustering is disabled, the simulation will have started in the setData function
  14056. if (this.constants.clustering.enabled) {
  14057. this.startWithClustering();
  14058. }
  14059. }
  14060. // Extend Graph with an Emitter mixin
  14061. Emitter(Graph.prototype);
  14062. /**
  14063. * Get the script path where the vis.js library is located
  14064. *
  14065. * @returns {string | null} path Path or null when not found. Path does not
  14066. * end with a slash.
  14067. * @private
  14068. */
  14069. Graph.prototype._getScriptPath = function() {
  14070. var scripts = document.getElementsByTagName( 'script' );
  14071. // find script named vis.js or vis.min.js
  14072. for (var i = 0; i < scripts.length; i++) {
  14073. var src = scripts[i].src;
  14074. var match = src && /\/?vis(.min)?\.js$/.exec(src);
  14075. if (match) {
  14076. // return path without the script name
  14077. return src.substring(0, src.length - match[0].length);
  14078. }
  14079. }
  14080. return null;
  14081. };
  14082. /**
  14083. * Find the center position of the graph
  14084. * @private
  14085. */
  14086. Graph.prototype._getRange = function() {
  14087. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  14088. for (var nodeId in this.nodes) {
  14089. if (this.nodes.hasOwnProperty(nodeId)) {
  14090. node = this.nodes[nodeId];
  14091. if (minX > (node.x)) {minX = node.x;}
  14092. if (maxX < (node.x)) {maxX = node.x;}
  14093. if (minY > (node.y)) {minY = node.y;}
  14094. if (maxY < (node.y)) {maxY = node.y;}
  14095. }
  14096. }
  14097. if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) {
  14098. minY = 0, maxY = 0, minX = 0, maxX = 0;
  14099. }
  14100. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14101. };
  14102. /**
  14103. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14104. * @returns {{x: number, y: number}}
  14105. * @private
  14106. */
  14107. Graph.prototype._findCenter = function(range) {
  14108. return {x: (0.5 * (range.maxX + range.minX)),
  14109. y: (0.5 * (range.maxY + range.minY))};
  14110. };
  14111. /**
  14112. * center the graph
  14113. *
  14114. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  14115. */
  14116. Graph.prototype._centerGraph = function(range) {
  14117. var center = this._findCenter(range);
  14118. center.x *= this.scale;
  14119. center.y *= this.scale;
  14120. center.x -= 0.5 * this.frame.canvas.clientWidth;
  14121. center.y -= 0.5 * this.frame.canvas.clientHeight;
  14122. this._setTranslation(-center.x,-center.y); // set at 0,0
  14123. };
  14124. /**
  14125. * This function zooms out to fit all data on screen based on amount of nodes
  14126. *
  14127. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  14128. * @param {Boolean} [disableStart] | If true, start is not called.
  14129. */
  14130. Graph.prototype.zoomExtent = function(initialZoom, disableStart) {
  14131. if (initialZoom === undefined) {
  14132. initialZoom = false;
  14133. }
  14134. if (disableStart === undefined) {
  14135. disableStart = false;
  14136. }
  14137. var range = this._getRange();
  14138. var zoomLevel;
  14139. if (initialZoom == true) {
  14140. var numberOfNodes = this.nodeIndices.length;
  14141. if (this.constants.smoothCurves == true) {
  14142. if (this.constants.clustering.enabled == true &&
  14143. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14144. 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.
  14145. }
  14146. else {
  14147. zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14148. }
  14149. }
  14150. else {
  14151. if (this.constants.clustering.enabled == true &&
  14152. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  14153. 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.
  14154. }
  14155. else {
  14156. zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14157. }
  14158. }
  14159. // correct for larger canvasses.
  14160. var factor = Math.min(this.frame.canvas.clientWidth / 600, this.frame.canvas.clientHeight / 600);
  14161. zoomLevel *= factor;
  14162. }
  14163. else {
  14164. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  14165. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  14166. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  14167. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  14168. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  14169. }
  14170. if (zoomLevel > 1.0) {
  14171. zoomLevel = 1.0;
  14172. }
  14173. this._setScale(zoomLevel);
  14174. this._centerGraph(range);
  14175. if (disableStart == false) {
  14176. this.moving = true;
  14177. this.start();
  14178. }
  14179. };
  14180. /**
  14181. * Update the this.nodeIndices with the most recent node index list
  14182. * @private
  14183. */
  14184. Graph.prototype._updateNodeIndexList = function() {
  14185. this._clearNodeIndexList();
  14186. for (var idx in this.nodes) {
  14187. if (this.nodes.hasOwnProperty(idx)) {
  14188. this.nodeIndices.push(idx);
  14189. }
  14190. }
  14191. };
  14192. /**
  14193. * Set nodes and edges, and optionally options as well.
  14194. *
  14195. * @param {Object} data Object containing parameters:
  14196. * {Array | DataSet | DataView} [nodes] Array with nodes
  14197. * {Array | DataSet | DataView} [edges] Array with edges
  14198. * {String} [dot] String containing data in DOT format
  14199. * {Options} [options] Object with options
  14200. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  14201. */
  14202. Graph.prototype.setData = function(data, disableStart) {
  14203. if (disableStart === undefined) {
  14204. disableStart = false;
  14205. }
  14206. if (data && data.dot && (data.nodes || data.edges)) {
  14207. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  14208. ' parameter pair "nodes" and "edges", but not both.');
  14209. }
  14210. // set options
  14211. this.setOptions(data && data.options);
  14212. // set all data
  14213. if (data && data.dot) {
  14214. // parse DOT file
  14215. if(data && data.dot) {
  14216. var dotData = vis.util.DOTToGraph(data.dot);
  14217. this.setData(dotData);
  14218. return;
  14219. }
  14220. }
  14221. else {
  14222. this._setNodes(data && data.nodes);
  14223. this._setEdges(data && data.edges);
  14224. }
  14225. this._putDataInSector();
  14226. if (!disableStart) {
  14227. // find a stable position or start animating to a stable position
  14228. if (this.stabilize) {
  14229. var me = this;
  14230. setTimeout(function() {me._stabilize(); me.start();},0)
  14231. }
  14232. else {
  14233. this.start();
  14234. }
  14235. }
  14236. };
  14237. /**
  14238. * Set options
  14239. * @param {Object} options
  14240. * @param {Boolean} [initializeView] | set zoom and translation to default.
  14241. */
  14242. Graph.prototype.setOptions = function (options) {
  14243. if (options) {
  14244. var prop;
  14245. // retrieve parameter values
  14246. if (options.width !== undefined) {this.width = options.width;}
  14247. if (options.height !== undefined) {this.height = options.height;}
  14248. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  14249. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  14250. if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;}
  14251. if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;}
  14252. if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;}
  14253. if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;}
  14254. if (options.dragGraph !== undefined) {this.constants.dragGraph = options.dragGraph;}
  14255. if (options.dragNodes !== undefined) {this.constants.dragNodes = options.dragNodes;}
  14256. if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
  14257. if (options.hover !== undefined) {this.constants.hover = options.hover;}
  14258. if (options.labels !== undefined) {
  14259. for (prop in options.labels) {
  14260. if (options.labels.hasOwnProperty(prop)) {
  14261. this.constants.labels[prop] = options.labels[prop];
  14262. }
  14263. }
  14264. }
  14265. if (options.onAdd) {
  14266. this.triggerFunctions.add = options.onAdd;
  14267. }
  14268. if (options.onEdit) {
  14269. this.triggerFunctions.edit = options.onEdit;
  14270. }
  14271. if (options.onConnect) {
  14272. this.triggerFunctions.connect = options.onConnect;
  14273. }
  14274. if (options.onDelete) {
  14275. this.triggerFunctions.del = options.onDelete;
  14276. }
  14277. if (options.physics) {
  14278. if (options.physics.barnesHut) {
  14279. this.constants.physics.barnesHut.enabled = true;
  14280. for (prop in options.physics.barnesHut) {
  14281. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  14282. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  14283. }
  14284. }
  14285. }
  14286. if (options.physics.repulsion) {
  14287. this.constants.physics.barnesHut.enabled = false;
  14288. for (prop in options.physics.repulsion) {
  14289. if (options.physics.repulsion.hasOwnProperty(prop)) {
  14290. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  14291. }
  14292. }
  14293. }
  14294. if (options.physics.hierarchicalRepulsion) {
  14295. this.constants.hierarchicalLayout.enabled = true;
  14296. this.constants.physics.hierarchicalRepulsion.enabled = true;
  14297. this.constants.physics.barnesHut.enabled = false;
  14298. for (prop in options.physics.hierarchicalRepulsion) {
  14299. if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
  14300. this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
  14301. }
  14302. }
  14303. }
  14304. }
  14305. if (options.hierarchicalLayout) {
  14306. this.constants.hierarchicalLayout.enabled = true;
  14307. for (prop in options.hierarchicalLayout) {
  14308. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  14309. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  14310. }
  14311. }
  14312. }
  14313. else if (options.hierarchicalLayout !== undefined) {
  14314. this.constants.hierarchicalLayout.enabled = false;
  14315. }
  14316. if (options.clustering) {
  14317. this.constants.clustering.enabled = true;
  14318. for (prop in options.clustering) {
  14319. if (options.clustering.hasOwnProperty(prop)) {
  14320. this.constants.clustering[prop] = options.clustering[prop];
  14321. }
  14322. }
  14323. }
  14324. else if (options.clustering !== undefined) {
  14325. this.constants.clustering.enabled = false;
  14326. }
  14327. if (options.navigation) {
  14328. this.constants.navigation.enabled = true;
  14329. for (prop in options.navigation) {
  14330. if (options.navigation.hasOwnProperty(prop)) {
  14331. this.constants.navigation[prop] = options.navigation[prop];
  14332. }
  14333. }
  14334. }
  14335. else if (options.navigation !== undefined) {
  14336. this.constants.navigation.enabled = false;
  14337. }
  14338. if (options.keyboard) {
  14339. this.constants.keyboard.enabled = true;
  14340. for (prop in options.keyboard) {
  14341. if (options.keyboard.hasOwnProperty(prop)) {
  14342. this.constants.keyboard[prop] = options.keyboard[prop];
  14343. }
  14344. }
  14345. }
  14346. else if (options.keyboard !== undefined) {
  14347. this.constants.keyboard.enabled = false;
  14348. }
  14349. if (options.dataManipulation) {
  14350. this.constants.dataManipulation.enabled = true;
  14351. for (prop in options.dataManipulation) {
  14352. if (options.dataManipulation.hasOwnProperty(prop)) {
  14353. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  14354. }
  14355. }
  14356. this.editMode = this.constants.dataManipulation.initiallyVisible;
  14357. }
  14358. else if (options.dataManipulation !== undefined) {
  14359. this.constants.dataManipulation.enabled = false;
  14360. }
  14361. // TODO: work out these options and document them
  14362. if (options.edges) {
  14363. for (prop in options.edges) {
  14364. if (options.edges.hasOwnProperty(prop)) {
  14365. if (typeof options.edges[prop] != "object") {
  14366. this.constants.edges[prop] = options.edges[prop];
  14367. }
  14368. }
  14369. }
  14370. if (options.edges.color !== undefined) {
  14371. if (util.isString(options.edges.color)) {
  14372. this.constants.edges.color = {};
  14373. this.constants.edges.color.color = options.edges.color;
  14374. this.constants.edges.color.highlight = options.edges.color;
  14375. this.constants.edges.color.hover = options.edges.color;
  14376. }
  14377. else {
  14378. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  14379. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  14380. if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
  14381. }
  14382. }
  14383. if (!options.edges.fontColor) {
  14384. if (options.edges.color !== undefined) {
  14385. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  14386. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  14387. }
  14388. }
  14389. // Added to support dashed lines
  14390. // David Jordan
  14391. // 2012-08-08
  14392. if (options.edges.dash) {
  14393. if (options.edges.dash.length !== undefined) {
  14394. this.constants.edges.dash.length = options.edges.dash.length;
  14395. }
  14396. if (options.edges.dash.gap !== undefined) {
  14397. this.constants.edges.dash.gap = options.edges.dash.gap;
  14398. }
  14399. if (options.edges.dash.altLength !== undefined) {
  14400. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  14401. }
  14402. }
  14403. }
  14404. if (options.nodes) {
  14405. for (prop in options.nodes) {
  14406. if (options.nodes.hasOwnProperty(prop)) {
  14407. this.constants.nodes[prop] = options.nodes[prop];
  14408. }
  14409. }
  14410. if (options.nodes.color) {
  14411. this.constants.nodes.color = util.parseColor(options.nodes.color);
  14412. }
  14413. /*
  14414. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  14415. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  14416. */
  14417. }
  14418. if (options.groups) {
  14419. for (var groupname in options.groups) {
  14420. if (options.groups.hasOwnProperty(groupname)) {
  14421. var group = options.groups[groupname];
  14422. this.groups.add(groupname, group);
  14423. }
  14424. }
  14425. }
  14426. if (options.tooltip) {
  14427. for (prop in options.tooltip) {
  14428. if (options.tooltip.hasOwnProperty(prop)) {
  14429. this.constants.tooltip[prop] = options.tooltip[prop];
  14430. }
  14431. }
  14432. if (options.tooltip.color) {
  14433. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  14434. }
  14435. }
  14436. }
  14437. // (Re)loading the mixins that can be enabled or disabled in the options.
  14438. // load the force calculation functions, grouped under the physics system.
  14439. this._loadPhysicsSystem();
  14440. // load the navigation system.
  14441. this._loadNavigationControls();
  14442. // load the data manipulation system
  14443. this._loadManipulationSystem();
  14444. // configure the smooth curves
  14445. this._configureSmoothCurves();
  14446. // bind keys. If disabled, this will not do anything;
  14447. this._createKeyBinds();
  14448. this.setSize(this.width, this.height);
  14449. this.moving = true;
  14450. this.start();
  14451. };
  14452. /**
  14453. * Create the main frame for the Graph.
  14454. * This function is executed once when a Graph object is created. The frame
  14455. * contains a canvas, and this canvas contains all objects like the axis and
  14456. * nodes.
  14457. * @private
  14458. */
  14459. Graph.prototype._create = function () {
  14460. // remove all elements from the container element.
  14461. while (this.containerElement.hasChildNodes()) {
  14462. this.containerElement.removeChild(this.containerElement.firstChild);
  14463. }
  14464. this.frame = document.createElement('div');
  14465. this.frame.className = 'graph-frame';
  14466. this.frame.style.position = 'relative';
  14467. this.frame.style.overflow = 'hidden';
  14468. // create the graph canvas (HTML canvas element)
  14469. this.frame.canvas = document.createElement( 'canvas' );
  14470. this.frame.canvas.style.position = 'relative';
  14471. this.frame.appendChild(this.frame.canvas);
  14472. if (!this.frame.canvas.getContext) {
  14473. var noCanvas = document.createElement( 'DIV' );
  14474. noCanvas.style.color = 'red';
  14475. noCanvas.style.fontWeight = 'bold' ;
  14476. noCanvas.style.padding = '10px';
  14477. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14478. this.frame.canvas.appendChild(noCanvas);
  14479. }
  14480. var me = this;
  14481. this.drag = {};
  14482. this.pinch = {};
  14483. this.hammer = Hammer(this.frame.canvas, {
  14484. prevent_default: true
  14485. });
  14486. this.hammer.on('tap', me._onTap.bind(me) );
  14487. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  14488. this.hammer.on('hold', me._onHold.bind(me) );
  14489. this.hammer.on('pinch', me._onPinch.bind(me) );
  14490. this.hammer.on('touch', me._onTouch.bind(me) );
  14491. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14492. this.hammer.on('drag', me._onDrag.bind(me) );
  14493. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14494. this.hammer.on('release', me._onRelease.bind(me) );
  14495. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14496. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14497. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14498. // add the frame to the container element
  14499. this.containerElement.appendChild(this.frame);
  14500. };
  14501. /**
  14502. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  14503. * @private
  14504. */
  14505. Graph.prototype._createKeyBinds = function() {
  14506. var me = this;
  14507. this.mousetrap = mousetrap;
  14508. this.mousetrap.reset();
  14509. if (this.constants.keyboard.enabled == true) {
  14510. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  14511. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  14512. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  14513. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  14514. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  14515. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  14516. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  14517. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  14518. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  14519. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  14520. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  14521. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  14522. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  14523. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  14524. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  14525. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  14526. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  14527. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  14528. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  14529. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  14530. }
  14531. if (this.constants.dataManipulation.enabled == true) {
  14532. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  14533. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  14534. }
  14535. };
  14536. /**
  14537. * Get the pointer location from a touch location
  14538. * @param {{pageX: Number, pageY: Number}} touch
  14539. * @return {{x: Number, y: Number}} pointer
  14540. * @private
  14541. */
  14542. Graph.prototype._getPointer = function (touch) {
  14543. return {
  14544. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14545. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14546. };
  14547. };
  14548. /**
  14549. * On start of a touch gesture, store the pointer
  14550. * @param event
  14551. * @private
  14552. */
  14553. Graph.prototype._onTouch = function (event) {
  14554. this.drag.pointer = this._getPointer(event.gesture.center);
  14555. this.drag.pinched = false;
  14556. this.pinch.scale = this._getScale();
  14557. this._handleTouch(this.drag.pointer);
  14558. };
  14559. /**
  14560. * handle drag start event
  14561. * @private
  14562. */
  14563. Graph.prototype._onDragStart = function () {
  14564. this._handleDragStart();
  14565. };
  14566. /**
  14567. * This function is called by _onDragStart.
  14568. * It is separated out because we can then overload it for the datamanipulation system.
  14569. *
  14570. * @private
  14571. */
  14572. Graph.prototype._handleDragStart = function() {
  14573. var drag = this.drag;
  14574. var node = this._getNodeAt(drag.pointer);
  14575. // note: drag.pointer is set in _onTouch to get the initial touch location
  14576. drag.dragging = true;
  14577. drag.selection = [];
  14578. drag.translation = this._getTranslation();
  14579. drag.nodeId = null;
  14580. if (node != null) {
  14581. drag.nodeId = node.id;
  14582. // select the clicked node if not yet selected
  14583. if (!node.isSelected()) {
  14584. this._selectObject(node,false);
  14585. }
  14586. // create an array with the selected nodes and their original location and status
  14587. for (var objectId in this.selectionObj.nodes) {
  14588. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  14589. var object = this.selectionObj.nodes[objectId];
  14590. var s = {
  14591. id: object.id,
  14592. node: object,
  14593. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14594. x: object.x,
  14595. y: object.y,
  14596. xFixed: object.xFixed,
  14597. yFixed: object.yFixed
  14598. };
  14599. object.xFixed = true;
  14600. object.yFixed = true;
  14601. drag.selection.push(s);
  14602. }
  14603. }
  14604. }
  14605. };
  14606. /**
  14607. * handle drag event
  14608. * @private
  14609. */
  14610. Graph.prototype._onDrag = function (event) {
  14611. this._handleOnDrag(event)
  14612. };
  14613. /**
  14614. * This function is called by _onDrag.
  14615. * It is separated out because we can then overload it for the datamanipulation system.
  14616. *
  14617. * @private
  14618. */
  14619. Graph.prototype._handleOnDrag = function(event) {
  14620. if (this.drag.pinched) {
  14621. return;
  14622. }
  14623. var pointer = this._getPointer(event.gesture.center);
  14624. var me = this,
  14625. drag = this.drag,
  14626. selection = drag.selection;
  14627. if (selection && selection.length && this.constants.dragNodes == true) {
  14628. // calculate delta's and new location
  14629. var deltaX = pointer.x - drag.pointer.x,
  14630. deltaY = pointer.y - drag.pointer.y;
  14631. // update position of all selected nodes
  14632. selection.forEach(function (s) {
  14633. var node = s.node;
  14634. if (!s.xFixed) {
  14635. node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
  14636. }
  14637. if (!s.yFixed) {
  14638. node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
  14639. }
  14640. });
  14641. // start _animationStep if not yet running
  14642. if (!this.moving) {
  14643. this.moving = true;
  14644. this.start();
  14645. }
  14646. }
  14647. else {
  14648. if (this.constants.dragGraph == true) {
  14649. // move the graph
  14650. var diffX = pointer.x - this.drag.pointer.x;
  14651. var diffY = pointer.y - this.drag.pointer.y;
  14652. this._setTranslation(
  14653. this.drag.translation.x + diffX,
  14654. this.drag.translation.y + diffY);
  14655. this._redraw();
  14656. this.moving = true;
  14657. this.start();
  14658. }
  14659. }
  14660. };
  14661. /**
  14662. * handle drag start event
  14663. * @private
  14664. */
  14665. Graph.prototype._onDragEnd = function () {
  14666. this.drag.dragging = false;
  14667. var selection = this.drag.selection;
  14668. if (selection) {
  14669. selection.forEach(function (s) {
  14670. // restore original xFixed and yFixed
  14671. s.node.xFixed = s.xFixed;
  14672. s.node.yFixed = s.yFixed;
  14673. });
  14674. }
  14675. };
  14676. /**
  14677. * handle tap/click event: select/unselect a node
  14678. * @private
  14679. */
  14680. Graph.prototype._onTap = function (event) {
  14681. var pointer = this._getPointer(event.gesture.center);
  14682. this.pointerPosition = pointer;
  14683. this._handleTap(pointer);
  14684. };
  14685. /**
  14686. * handle doubletap event
  14687. * @private
  14688. */
  14689. Graph.prototype._onDoubleTap = function (event) {
  14690. var pointer = this._getPointer(event.gesture.center);
  14691. this._handleDoubleTap(pointer);
  14692. };
  14693. /**
  14694. * handle long tap event: multi select nodes
  14695. * @private
  14696. */
  14697. Graph.prototype._onHold = function (event) {
  14698. var pointer = this._getPointer(event.gesture.center);
  14699. this.pointerPosition = pointer;
  14700. this._handleOnHold(pointer);
  14701. };
  14702. /**
  14703. * handle the release of the screen
  14704. *
  14705. * @private
  14706. */
  14707. Graph.prototype._onRelease = function (event) {
  14708. var pointer = this._getPointer(event.gesture.center);
  14709. this._handleOnRelease(pointer);
  14710. };
  14711. /**
  14712. * Handle pinch event
  14713. * @param event
  14714. * @private
  14715. */
  14716. Graph.prototype._onPinch = function (event) {
  14717. var pointer = this._getPointer(event.gesture.center);
  14718. this.drag.pinched = true;
  14719. if (!('scale' in this.pinch)) {
  14720. this.pinch.scale = 1;
  14721. }
  14722. // TODO: enabled moving while pinching?
  14723. var scale = this.pinch.scale * event.gesture.scale;
  14724. this._zoom(scale, pointer)
  14725. };
  14726. /**
  14727. * Zoom the graph in or out
  14728. * @param {Number} scale a number around 1, and between 0.01 and 10
  14729. * @param {{x: Number, y: Number}} pointer Position on screen
  14730. * @return {Number} appliedScale scale is limited within the boundaries
  14731. * @private
  14732. */
  14733. Graph.prototype._zoom = function(scale, pointer) {
  14734. if (this.constants.zoomable == true) {
  14735. var scaleOld = this._getScale();
  14736. if (scale < 0.00001) {
  14737. scale = 0.00001;
  14738. }
  14739. if (scale > 10) {
  14740. scale = 10;
  14741. }
  14742. // + this.frame.canvas.clientHeight / 2
  14743. var translation = this._getTranslation();
  14744. var scaleFrac = scale / scaleOld;
  14745. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14746. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14747. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  14748. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  14749. this._setScale(scale);
  14750. this._setTranslation(tx, ty);
  14751. this.updateClustersDefault();
  14752. this._redraw();
  14753. if (scaleOld < scale) {
  14754. this.emit("zoom", {direction:"+"});
  14755. }
  14756. else {
  14757. this.emit("zoom", {direction:"-"});
  14758. }
  14759. return scale;
  14760. }
  14761. };
  14762. /**
  14763. * Event handler for mouse wheel event, used to zoom the timeline
  14764. * See http://adomas.org/javascript-mouse-wheel/
  14765. * https://github.com/EightMedia/hammer.js/issues/256
  14766. * @param {MouseEvent} event
  14767. * @private
  14768. */
  14769. Graph.prototype._onMouseWheel = function(event) {
  14770. // retrieve delta
  14771. var delta = 0;
  14772. if (event.wheelDelta) { /* IE/Opera. */
  14773. delta = event.wheelDelta/120;
  14774. } else if (event.detail) { /* Mozilla case. */
  14775. // In Mozilla, sign of delta is different than in IE.
  14776. // Also, delta is multiple of 3.
  14777. delta = -event.detail/3;
  14778. }
  14779. // If delta is nonzero, handle it.
  14780. // Basically, delta is now positive if wheel was scrolled up,
  14781. // and negative, if wheel was scrolled down.
  14782. if (delta) {
  14783. // calculate the new scale
  14784. var scale = this._getScale();
  14785. var zoom = delta / 10;
  14786. if (delta < 0) {
  14787. zoom = zoom / (1 - zoom);
  14788. }
  14789. scale *= (1 + zoom);
  14790. // calculate the pointer location
  14791. var gesture = util.fakeGesture(this, event);
  14792. var pointer = this._getPointer(gesture.center);
  14793. // apply the new scale
  14794. this._zoom(scale, pointer);
  14795. }
  14796. // Prevent default actions caused by mouse wheel.
  14797. event.preventDefault();
  14798. };
  14799. /**
  14800. * Mouse move handler for checking whether the title moves over a node with a title.
  14801. * @param {Event} event
  14802. * @private
  14803. */
  14804. Graph.prototype._onMouseMoveTitle = function (event) {
  14805. var gesture = util.fakeGesture(this, event);
  14806. var pointer = this._getPointer(gesture.center);
  14807. // check if the previously selected node is still selected
  14808. if (this.popupObj) {
  14809. this._checkHidePopup(pointer);
  14810. }
  14811. // start a timeout that will check if the mouse is positioned above
  14812. // an element
  14813. var me = this;
  14814. var checkShow = function() {
  14815. me._checkShowPopup(pointer);
  14816. };
  14817. if (this.popupTimer) {
  14818. clearInterval(this.popupTimer); // stop any running calculationTimer
  14819. }
  14820. if (!this.drag.dragging) {
  14821. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  14822. }
  14823. /**
  14824. * Adding hover highlights
  14825. */
  14826. if (this.constants.hover == true) {
  14827. // removing all hover highlights
  14828. for (var edgeId in this.hoverObj.edges) {
  14829. if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  14830. this.hoverObj.edges[edgeId].hover = false;
  14831. delete this.hoverObj.edges[edgeId];
  14832. }
  14833. }
  14834. // adding hover highlights
  14835. var obj = this._getNodeAt(pointer);
  14836. if (obj == null) {
  14837. obj = this._getEdgeAt(pointer);
  14838. }
  14839. if (obj != null) {
  14840. this._hoverObject(obj);
  14841. }
  14842. // removing all node hover highlights except for the selected one.
  14843. for (var nodeId in this.hoverObj.nodes) {
  14844. if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  14845. if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
  14846. this._blurObject(this.hoverObj.nodes[nodeId]);
  14847. delete this.hoverObj.nodes[nodeId];
  14848. }
  14849. }
  14850. }
  14851. this.redraw();
  14852. }
  14853. };
  14854. /**
  14855. * Check if there is an element on the given position in the graph
  14856. * (a node or edge). If so, and if this element has a title,
  14857. * show a popup window with its title.
  14858. *
  14859. * @param {{x:Number, y:Number}} pointer
  14860. * @private
  14861. */
  14862. Graph.prototype._checkShowPopup = function (pointer) {
  14863. var obj = {
  14864. left: this._XconvertDOMtoCanvas(pointer.x),
  14865. top: this._YconvertDOMtoCanvas(pointer.y),
  14866. right: this._XconvertDOMtoCanvas(pointer.x),
  14867. bottom: this._YconvertDOMtoCanvas(pointer.y)
  14868. };
  14869. var id;
  14870. var lastPopupNode = this.popupObj;
  14871. if (this.popupObj == undefined) {
  14872. // search the nodes for overlap, select the top one in case of multiple nodes
  14873. var nodes = this.nodes;
  14874. for (id in nodes) {
  14875. if (nodes.hasOwnProperty(id)) {
  14876. var node = nodes[id];
  14877. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  14878. this.popupObj = node;
  14879. break;
  14880. }
  14881. }
  14882. }
  14883. }
  14884. if (this.popupObj === undefined) {
  14885. // search the edges for overlap
  14886. var edges = this.edges;
  14887. for (id in edges) {
  14888. if (edges.hasOwnProperty(id)) {
  14889. var edge = edges[id];
  14890. if (edge.connected && (edge.getTitle() !== undefined) &&
  14891. edge.isOverlappingWith(obj)) {
  14892. this.popupObj = edge;
  14893. break;
  14894. }
  14895. }
  14896. }
  14897. }
  14898. if (this.popupObj) {
  14899. // show popup message window
  14900. if (this.popupObj != lastPopupNode) {
  14901. var me = this;
  14902. if (!me.popup) {
  14903. me.popup = new Popup(me.frame, me.constants.tooltip);
  14904. }
  14905. // adjust a small offset such that the mouse cursor is located in the
  14906. // bottom left location of the popup, and you can easily move over the
  14907. // popup area
  14908. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  14909. me.popup.setText(me.popupObj.getTitle());
  14910. me.popup.show();
  14911. }
  14912. }
  14913. else {
  14914. if (this.popup) {
  14915. this.popup.hide();
  14916. }
  14917. }
  14918. };
  14919. /**
  14920. * Check if the popup must be hided, which is the case when the mouse is no
  14921. * longer hovering on the object
  14922. * @param {{x:Number, y:Number}} pointer
  14923. * @private
  14924. */
  14925. Graph.prototype._checkHidePopup = function (pointer) {
  14926. if (!this.popupObj || !this._getNodeAt(pointer) ) {
  14927. this.popupObj = undefined;
  14928. if (this.popup) {
  14929. this.popup.hide();
  14930. }
  14931. }
  14932. };
  14933. /**
  14934. * Set a new size for the graph
  14935. * @param {string} width Width in pixels or percentage (for example '800px'
  14936. * or '50%')
  14937. * @param {string} height Height in pixels or percentage (for example '400px'
  14938. * or '30%')
  14939. */
  14940. Graph.prototype.setSize = function(width, height) {
  14941. this.frame.style.width = width;
  14942. this.frame.style.height = height;
  14943. this.frame.canvas.style.width = '100%';
  14944. this.frame.canvas.style.height = '100%';
  14945. this.frame.canvas.width = this.frame.canvas.clientWidth;
  14946. this.frame.canvas.height = this.frame.canvas.clientHeight;
  14947. if (this.manipulationDiv !== undefined) {
  14948. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  14949. }
  14950. if (this.navigationDivs !== undefined) {
  14951. if (this.navigationDivs['wrapper'] !== undefined) {
  14952. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  14953. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  14954. }
  14955. }
  14956. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  14957. };
  14958. /**
  14959. * Set a data set with nodes for the graph
  14960. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  14961. * @private
  14962. */
  14963. Graph.prototype._setNodes = function(nodes) {
  14964. var oldNodesData = this.nodesData;
  14965. if (nodes instanceof DataSet || nodes instanceof DataView) {
  14966. this.nodesData = nodes;
  14967. }
  14968. else if (nodes instanceof Array) {
  14969. this.nodesData = new DataSet();
  14970. this.nodesData.add(nodes);
  14971. }
  14972. else if (!nodes) {
  14973. this.nodesData = new DataSet();
  14974. }
  14975. else {
  14976. throw new TypeError('Array or DataSet expected');
  14977. }
  14978. if (oldNodesData) {
  14979. // unsubscribe from old dataset
  14980. util.forEach(this.nodesListeners, function (callback, event) {
  14981. oldNodesData.off(event, callback);
  14982. });
  14983. }
  14984. // remove drawn nodes
  14985. this.nodes = {};
  14986. if (this.nodesData) {
  14987. // subscribe to new dataset
  14988. var me = this;
  14989. util.forEach(this.nodesListeners, function (callback, event) {
  14990. me.nodesData.on(event, callback);
  14991. });
  14992. // draw all new nodes
  14993. var ids = this.nodesData.getIds();
  14994. this._addNodes(ids);
  14995. }
  14996. this._updateSelection();
  14997. };
  14998. /**
  14999. * Add nodes
  15000. * @param {Number[] | String[]} ids
  15001. * @private
  15002. */
  15003. Graph.prototype._addNodes = function(ids) {
  15004. var id;
  15005. for (var i = 0, len = ids.length; i < len; i++) {
  15006. id = ids[i];
  15007. var data = this.nodesData.get(id);
  15008. var node = new Node(data, this.images, this.groups, this.constants);
  15009. this.nodes[id] = node; // note: this may replace an existing node
  15010. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  15011. var radius = 10 * 0.1*ids.length;
  15012. var angle = 2 * Math.PI * Math.random();
  15013. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  15014. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  15015. }
  15016. this.moving = true;
  15017. }
  15018. this._updateNodeIndexList();
  15019. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15020. this._resetLevels();
  15021. this._setupHierarchicalLayout();
  15022. }
  15023. this._updateCalculationNodes();
  15024. this._reconnectEdges();
  15025. this._updateValueRange(this.nodes);
  15026. this.updateLabels();
  15027. };
  15028. /**
  15029. * Update existing nodes, or create them when not yet existing
  15030. * @param {Number[] | String[]} ids
  15031. * @private
  15032. */
  15033. Graph.prototype._updateNodes = function(ids) {
  15034. var nodes = this.nodes,
  15035. nodesData = this.nodesData;
  15036. for (var i = 0, len = ids.length; i < len; i++) {
  15037. var id = ids[i];
  15038. var node = nodes[id];
  15039. var data = nodesData.get(id);
  15040. if (node) {
  15041. // update node
  15042. node.setProperties(data, this.constants);
  15043. }
  15044. else {
  15045. // create node
  15046. node = new Node(properties, this.images, this.groups, this.constants);
  15047. nodes[id] = node;
  15048. }
  15049. }
  15050. this.moving = true;
  15051. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15052. this._resetLevels();
  15053. this._setupHierarchicalLayout();
  15054. }
  15055. this._updateNodeIndexList();
  15056. this._reconnectEdges();
  15057. this._updateValueRange(nodes);
  15058. };
  15059. /**
  15060. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  15061. * @param {Number[] | String[]} ids
  15062. * @private
  15063. */
  15064. Graph.prototype._removeNodes = function(ids) {
  15065. var nodes = this.nodes;
  15066. for (var i = 0, len = ids.length; i < len; i++) {
  15067. var id = ids[i];
  15068. delete nodes[id];
  15069. }
  15070. this._updateNodeIndexList();
  15071. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15072. this._resetLevels();
  15073. this._setupHierarchicalLayout();
  15074. }
  15075. this._updateCalculationNodes();
  15076. this._reconnectEdges();
  15077. this._updateSelection();
  15078. this._updateValueRange(nodes);
  15079. };
  15080. /**
  15081. * Load edges by reading the data table
  15082. * @param {Array | DataSet | DataView} edges The data containing the edges.
  15083. * @private
  15084. * @private
  15085. */
  15086. Graph.prototype._setEdges = function(edges) {
  15087. var oldEdgesData = this.edgesData;
  15088. if (edges instanceof DataSet || edges instanceof DataView) {
  15089. this.edgesData = edges;
  15090. }
  15091. else if (edges instanceof Array) {
  15092. this.edgesData = new DataSet();
  15093. this.edgesData.add(edges);
  15094. }
  15095. else if (!edges) {
  15096. this.edgesData = new DataSet();
  15097. }
  15098. else {
  15099. throw new TypeError('Array or DataSet expected');
  15100. }
  15101. if (oldEdgesData) {
  15102. // unsubscribe from old dataset
  15103. util.forEach(this.edgesListeners, function (callback, event) {
  15104. oldEdgesData.off(event, callback);
  15105. });
  15106. }
  15107. // remove drawn edges
  15108. this.edges = {};
  15109. if (this.edgesData) {
  15110. // subscribe to new dataset
  15111. var me = this;
  15112. util.forEach(this.edgesListeners, function (callback, event) {
  15113. me.edgesData.on(event, callback);
  15114. });
  15115. // draw all new nodes
  15116. var ids = this.edgesData.getIds();
  15117. this._addEdges(ids);
  15118. }
  15119. this._reconnectEdges();
  15120. };
  15121. /**
  15122. * Add edges
  15123. * @param {Number[] | String[]} ids
  15124. * @private
  15125. */
  15126. Graph.prototype._addEdges = function (ids) {
  15127. var edges = this.edges,
  15128. edgesData = this.edgesData;
  15129. for (var i = 0, len = ids.length; i < len; i++) {
  15130. var id = ids[i];
  15131. var oldEdge = edges[id];
  15132. if (oldEdge) {
  15133. oldEdge.disconnect();
  15134. }
  15135. var data = edgesData.get(id, {"showInternalIds" : true});
  15136. edges[id] = new Edge(data, this, this.constants);
  15137. }
  15138. this.moving = true;
  15139. this._updateValueRange(edges);
  15140. this._createBezierNodes();
  15141. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15142. this._resetLevels();
  15143. this._setupHierarchicalLayout();
  15144. }
  15145. this._updateCalculationNodes();
  15146. };
  15147. /**
  15148. * Update existing edges, or create them when not yet existing
  15149. * @param {Number[] | String[]} ids
  15150. * @private
  15151. */
  15152. Graph.prototype._updateEdges = function (ids) {
  15153. var edges = this.edges,
  15154. edgesData = this.edgesData;
  15155. for (var i = 0, len = ids.length; i < len; i++) {
  15156. var id = ids[i];
  15157. var data = edgesData.get(id);
  15158. var edge = edges[id];
  15159. if (edge) {
  15160. // update edge
  15161. edge.disconnect();
  15162. edge.setProperties(data, this.constants);
  15163. edge.connect();
  15164. }
  15165. else {
  15166. // create edge
  15167. edge = new Edge(data, this, this.constants);
  15168. this.edges[id] = edge;
  15169. }
  15170. }
  15171. this._createBezierNodes();
  15172. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15173. this._resetLevels();
  15174. this._setupHierarchicalLayout();
  15175. }
  15176. this.moving = true;
  15177. this._updateValueRange(edges);
  15178. };
  15179. /**
  15180. * Remove existing edges. Non existing ids will be ignored
  15181. * @param {Number[] | String[]} ids
  15182. * @private
  15183. */
  15184. Graph.prototype._removeEdges = function (ids) {
  15185. var edges = this.edges;
  15186. for (var i = 0, len = ids.length; i < len; i++) {
  15187. var id = ids[i];
  15188. var edge = edges[id];
  15189. if (edge) {
  15190. if (edge.via != null) {
  15191. delete this.sectors['support']['nodes'][edge.via.id];
  15192. }
  15193. edge.disconnect();
  15194. delete edges[id];
  15195. }
  15196. }
  15197. this.moving = true;
  15198. this._updateValueRange(edges);
  15199. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15200. this._resetLevels();
  15201. this._setupHierarchicalLayout();
  15202. }
  15203. this._updateCalculationNodes();
  15204. };
  15205. /**
  15206. * Reconnect all edges
  15207. * @private
  15208. */
  15209. Graph.prototype._reconnectEdges = function() {
  15210. var id,
  15211. nodes = this.nodes,
  15212. edges = this.edges;
  15213. for (id in nodes) {
  15214. if (nodes.hasOwnProperty(id)) {
  15215. nodes[id].edges = [];
  15216. }
  15217. }
  15218. for (id in edges) {
  15219. if (edges.hasOwnProperty(id)) {
  15220. var edge = edges[id];
  15221. edge.from = null;
  15222. edge.to = null;
  15223. edge.connect();
  15224. }
  15225. }
  15226. };
  15227. /**
  15228. * Update the values of all object in the given array according to the current
  15229. * value range of the objects in the array.
  15230. * @param {Object} obj An object containing a set of Edges or Nodes
  15231. * The objects must have a method getValue() and
  15232. * setValueRange(min, max).
  15233. * @private
  15234. */
  15235. Graph.prototype._updateValueRange = function(obj) {
  15236. var id;
  15237. // determine the range of the objects
  15238. var valueMin = undefined;
  15239. var valueMax = undefined;
  15240. for (id in obj) {
  15241. if (obj.hasOwnProperty(id)) {
  15242. var value = obj[id].getValue();
  15243. if (value !== undefined) {
  15244. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  15245. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  15246. }
  15247. }
  15248. }
  15249. // adjust the range of all objects
  15250. if (valueMin !== undefined && valueMax !== undefined) {
  15251. for (id in obj) {
  15252. if (obj.hasOwnProperty(id)) {
  15253. obj[id].setValueRange(valueMin, valueMax);
  15254. }
  15255. }
  15256. }
  15257. };
  15258. /**
  15259. * Redraw the graph with the current data
  15260. * chart will be resized too.
  15261. */
  15262. Graph.prototype.redraw = function() {
  15263. this.setSize(this.width, this.height);
  15264. this._redraw();
  15265. };
  15266. /**
  15267. * Redraw the graph with the current data
  15268. * @private
  15269. */
  15270. Graph.prototype._redraw = function() {
  15271. var ctx = this.frame.canvas.getContext('2d');
  15272. // clear the canvas
  15273. var w = this.frame.canvas.width;
  15274. var h = this.frame.canvas.height;
  15275. ctx.clearRect(0, 0, w, h);
  15276. // set scaling and translation
  15277. ctx.save();
  15278. ctx.translate(this.translation.x, this.translation.y);
  15279. ctx.scale(this.scale, this.scale);
  15280. this.canvasTopLeft = {
  15281. "x": this._XconvertDOMtoCanvas(0),
  15282. "y": this._YconvertDOMtoCanvas(0)
  15283. };
  15284. this.canvasBottomRight = {
  15285. "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
  15286. "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
  15287. };
  15288. this._doInAllSectors("_drawAllSectorNodes",ctx);
  15289. this._doInAllSectors("_drawEdges",ctx);
  15290. this._doInAllSectors("_drawNodes",ctx,false);
  15291. // this._doInSupportSector("_drawNodes",ctx,true);
  15292. // this._drawTree(ctx,"#F00F0F");
  15293. // restore original scaling and translation
  15294. ctx.restore();
  15295. };
  15296. /**
  15297. * Set the translation of the graph
  15298. * @param {Number} offsetX Horizontal offset
  15299. * @param {Number} offsetY Vertical offset
  15300. * @private
  15301. */
  15302. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  15303. if (this.translation === undefined) {
  15304. this.translation = {
  15305. x: 0,
  15306. y: 0
  15307. };
  15308. }
  15309. if (offsetX !== undefined) {
  15310. this.translation.x = offsetX;
  15311. }
  15312. if (offsetY !== undefined) {
  15313. this.translation.y = offsetY;
  15314. }
  15315. this.emit('viewChanged');
  15316. };
  15317. /**
  15318. * Get the translation of the graph
  15319. * @return {Object} translation An object with parameters x and y, both a number
  15320. * @private
  15321. */
  15322. Graph.prototype._getTranslation = function() {
  15323. return {
  15324. x: this.translation.x,
  15325. y: this.translation.y
  15326. };
  15327. };
  15328. /**
  15329. * Scale the graph
  15330. * @param {Number} scale Scaling factor 1.0 is unscaled
  15331. * @private
  15332. */
  15333. Graph.prototype._setScale = function(scale) {
  15334. this.scale = scale;
  15335. };
  15336. /**
  15337. * Get the current scale of the graph
  15338. * @return {Number} scale Scaling factor 1.0 is unscaled
  15339. * @private
  15340. */
  15341. Graph.prototype._getScale = function() {
  15342. return this.scale;
  15343. };
  15344. /**
  15345. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  15346. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  15347. * @param {number} x
  15348. * @returns {number}
  15349. * @private
  15350. */
  15351. Graph.prototype._XconvertDOMtoCanvas = function(x) {
  15352. return (x - this.translation.x) / this.scale;
  15353. };
  15354. /**
  15355. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  15356. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  15357. * @param {number} x
  15358. * @returns {number}
  15359. * @private
  15360. */
  15361. Graph.prototype._XconvertCanvasToDOM = function(x) {
  15362. return x * this.scale + this.translation.x;
  15363. };
  15364. /**
  15365. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  15366. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  15367. * @param {number} y
  15368. * @returns {number}
  15369. * @private
  15370. */
  15371. Graph.prototype._YconvertDOMtoCanvas = function(y) {
  15372. return (y - this.translation.y) / this.scale;
  15373. };
  15374. /**
  15375. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  15376. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  15377. * @param {number} y
  15378. * @returns {number}
  15379. * @private
  15380. */
  15381. Graph.prototype._YconvertCanvasToDOM = function(y) {
  15382. return y * this.scale + this.translation.y ;
  15383. };
  15384. /**
  15385. *
  15386. * @param {object} pos = {x: number, y: number}
  15387. * @returns {{x: number, y: number}}
  15388. * @constructor
  15389. */
  15390. Graph.prototype.canvasToDOM = function(pos) {
  15391. return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
  15392. }
  15393. /**
  15394. *
  15395. * @param {object} pos = {x: number, y: number}
  15396. * @returns {{x: number, y: number}}
  15397. * @constructor
  15398. */
  15399. Graph.prototype.DOMtoCanvas = function(pos) {
  15400. return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
  15401. }
  15402. /**
  15403. * Redraw all nodes
  15404. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15405. * @param {CanvasRenderingContext2D} ctx
  15406. * @param {Boolean} [alwaysShow]
  15407. * @private
  15408. */
  15409. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  15410. if (alwaysShow === undefined) {
  15411. alwaysShow = false;
  15412. }
  15413. // first draw the unselected nodes
  15414. var nodes = this.nodes;
  15415. var selected = [];
  15416. for (var id in nodes) {
  15417. if (nodes.hasOwnProperty(id)) {
  15418. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  15419. if (nodes[id].isSelected()) {
  15420. selected.push(id);
  15421. }
  15422. else {
  15423. if (nodes[id].inArea() || alwaysShow) {
  15424. nodes[id].draw(ctx);
  15425. }
  15426. }
  15427. }
  15428. }
  15429. // draw the selected nodes on top
  15430. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  15431. if (nodes[selected[s]].inArea() || alwaysShow) {
  15432. nodes[selected[s]].draw(ctx);
  15433. }
  15434. }
  15435. };
  15436. /**
  15437. * Redraw all edges
  15438. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15439. * @param {CanvasRenderingContext2D} ctx
  15440. * @private
  15441. */
  15442. Graph.prototype._drawEdges = function(ctx) {
  15443. var edges = this.edges;
  15444. for (var id in edges) {
  15445. if (edges.hasOwnProperty(id)) {
  15446. var edge = edges[id];
  15447. edge.setScale(this.scale);
  15448. if (edge.connected) {
  15449. edges[id].draw(ctx);
  15450. }
  15451. }
  15452. }
  15453. };
  15454. /**
  15455. * Find a stable position for all nodes
  15456. * @private
  15457. */
  15458. Graph.prototype._stabilize = function() {
  15459. if (this.constants.freezeForStabilization == true) {
  15460. this._freezeDefinedNodes();
  15461. }
  15462. // find stable position
  15463. var count = 0;
  15464. while (this.moving && count < this.constants.stabilizationIterations) {
  15465. this._physicsTick();
  15466. count++;
  15467. }
  15468. this.zoomExtent(false,true);
  15469. if (this.constants.freezeForStabilization == true) {
  15470. this._restoreFrozenNodes();
  15471. }
  15472. this.emit("stabilized",{iterations:count});
  15473. };
  15474. /**
  15475. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  15476. * because only the supportnodes for the smoothCurves have to settle.
  15477. *
  15478. * @private
  15479. */
  15480. Graph.prototype._freezeDefinedNodes = function() {
  15481. var nodes = this.nodes;
  15482. for (var id in nodes) {
  15483. if (nodes.hasOwnProperty(id)) {
  15484. if (nodes[id].x != null && nodes[id].y != null) {
  15485. nodes[id].fixedData.x = nodes[id].xFixed;
  15486. nodes[id].fixedData.y = nodes[id].yFixed;
  15487. nodes[id].xFixed = true;
  15488. nodes[id].yFixed = true;
  15489. }
  15490. }
  15491. }
  15492. };
  15493. /**
  15494. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  15495. *
  15496. * @private
  15497. */
  15498. Graph.prototype._restoreFrozenNodes = function() {
  15499. var nodes = this.nodes;
  15500. for (var id in nodes) {
  15501. if (nodes.hasOwnProperty(id)) {
  15502. if (nodes[id].fixedData.x != null) {
  15503. nodes[id].xFixed = nodes[id].fixedData.x;
  15504. nodes[id].yFixed = nodes[id].fixedData.y;
  15505. }
  15506. }
  15507. }
  15508. };
  15509. /**
  15510. * Check if any of the nodes is still moving
  15511. * @param {number} vmin the minimum velocity considered as 'moving'
  15512. * @return {boolean} true if moving, false if non of the nodes is moving
  15513. * @private
  15514. */
  15515. Graph.prototype._isMoving = function(vmin) {
  15516. var nodes = this.nodes;
  15517. for (var id in nodes) {
  15518. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15519. return true;
  15520. }
  15521. }
  15522. return false;
  15523. };
  15524. /**
  15525. * /**
  15526. * Perform one discrete step for all nodes
  15527. *
  15528. * @private
  15529. */
  15530. Graph.prototype._discreteStepNodes = function() {
  15531. var interval = this.physicsDiscreteStepsize;
  15532. var nodes = this.nodes;
  15533. var nodeId;
  15534. var nodesPresent = false;
  15535. if (this.constants.maxVelocity > 0) {
  15536. for (nodeId in nodes) {
  15537. if (nodes.hasOwnProperty(nodeId)) {
  15538. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  15539. nodesPresent = true;
  15540. }
  15541. }
  15542. }
  15543. else {
  15544. for (nodeId in nodes) {
  15545. if (nodes.hasOwnProperty(nodeId)) {
  15546. nodes[nodeId].discreteStep(interval);
  15547. nodesPresent = true;
  15548. }
  15549. }
  15550. }
  15551. if (nodesPresent == true) {
  15552. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  15553. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  15554. this.moving = true;
  15555. }
  15556. else {
  15557. this.moving = this._isMoving(vminCorrected);
  15558. }
  15559. }
  15560. };
  15561. /**
  15562. * A single simulation step (or "tick") in the physics simulation
  15563. *
  15564. * @private
  15565. */
  15566. Graph.prototype._physicsTick = function() {
  15567. if (!this.freezeSimulation) {
  15568. if (this.moving) {
  15569. this._doInAllActiveSectors("_initializeForceCalculation");
  15570. this._doInAllActiveSectors("_discreteStepNodes");
  15571. if (this.constants.smoothCurves) {
  15572. this._doInSupportSector("_discreteStepNodes");
  15573. }
  15574. this._findCenter(this._getRange())
  15575. }
  15576. }
  15577. };
  15578. /**
  15579. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  15580. * It reschedules itself at the beginning of the function
  15581. *
  15582. * @private
  15583. */
  15584. Graph.prototype._animationStep = function() {
  15585. // reset the timer so a new scheduled animation step can be set
  15586. this.timer = undefined;
  15587. // handle the keyboad movement
  15588. this._handleNavigation();
  15589. // this schedules a new animation step
  15590. this.start();
  15591. // start the physics simulation
  15592. var calculationTime = Date.now();
  15593. var maxSteps = 1;
  15594. this._physicsTick();
  15595. var timeRequired = Date.now() - calculationTime;
  15596. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  15597. this._physicsTick();
  15598. timeRequired = Date.now() - calculationTime;
  15599. maxSteps++;
  15600. }
  15601. // start the rendering process
  15602. var renderTime = Date.now();
  15603. this._redraw();
  15604. this.renderTime = Date.now() - renderTime;
  15605. };
  15606. if (typeof window !== 'undefined') {
  15607. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  15608. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  15609. }
  15610. /**
  15611. * Schedule a animation step with the refreshrate interval.
  15612. */
  15613. Graph.prototype.start = function() {
  15614. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  15615. if (!this.timer) {
  15616. var ua = navigator.userAgent.toLowerCase();
  15617. var requiresTimeout = false;
  15618. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  15619. requiresTimeout = true;
  15620. }
  15621. else if (ua.indexOf('safari') != -1) { // safari
  15622. if (ua.indexOf('chrome') <= -1) {
  15623. requiresTimeout = true;
  15624. }
  15625. }
  15626. if (requiresTimeout == true) {
  15627. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15628. }
  15629. else{
  15630. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15631. }
  15632. }
  15633. }
  15634. else {
  15635. this._redraw();
  15636. }
  15637. };
  15638. /**
  15639. * Move the graph according to the keyboard presses.
  15640. *
  15641. * @private
  15642. */
  15643. Graph.prototype._handleNavigation = function() {
  15644. if (this.xIncrement != 0 || this.yIncrement != 0) {
  15645. var translation = this._getTranslation();
  15646. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  15647. }
  15648. if (this.zoomIncrement != 0) {
  15649. var center = {
  15650. x: this.frame.canvas.clientWidth / 2,
  15651. y: this.frame.canvas.clientHeight / 2
  15652. };
  15653. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  15654. }
  15655. };
  15656. /**
  15657. * Freeze the _animationStep
  15658. */
  15659. Graph.prototype.toggleFreeze = function() {
  15660. if (this.freezeSimulation == false) {
  15661. this.freezeSimulation = true;
  15662. }
  15663. else {
  15664. this.freezeSimulation = false;
  15665. this.start();
  15666. }
  15667. };
  15668. /**
  15669. * This function cleans the support nodes if they are not needed and adds them when they are.
  15670. *
  15671. * @param {boolean} [disableStart]
  15672. * @private
  15673. */
  15674. Graph.prototype._configureSmoothCurves = function(disableStart) {
  15675. if (disableStart === undefined) {
  15676. disableStart = true;
  15677. }
  15678. if (this.constants.smoothCurves == true) {
  15679. this._createBezierNodes();
  15680. }
  15681. else {
  15682. // delete the support nodes
  15683. this.sectors['support']['nodes'] = {};
  15684. for (var edgeId in this.edges) {
  15685. if (this.edges.hasOwnProperty(edgeId)) {
  15686. this.edges[edgeId].smooth = false;
  15687. this.edges[edgeId].via = null;
  15688. }
  15689. }
  15690. }
  15691. this._updateCalculationNodes();
  15692. if (!disableStart) {
  15693. this.moving = true;
  15694. this.start();
  15695. }
  15696. };
  15697. /**
  15698. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  15699. * are used for the force calculation.
  15700. *
  15701. * @private
  15702. */
  15703. Graph.prototype._createBezierNodes = function() {
  15704. if (this.constants.smoothCurves == true) {
  15705. for (var edgeId in this.edges) {
  15706. if (this.edges.hasOwnProperty(edgeId)) {
  15707. var edge = this.edges[edgeId];
  15708. if (edge.via == null) {
  15709. edge.smooth = true;
  15710. var nodeId = "edgeId:".concat(edge.id);
  15711. this.sectors['support']['nodes'][nodeId] = new Node(
  15712. {id:nodeId,
  15713. mass:1,
  15714. shape:'circle',
  15715. image:"",
  15716. internalMultiplier:1
  15717. },{},{},this.constants);
  15718. edge.via = this.sectors['support']['nodes'][nodeId];
  15719. edge.via.parentEdgeId = edge.id;
  15720. edge.positionBezierNode();
  15721. }
  15722. }
  15723. }
  15724. }
  15725. };
  15726. /**
  15727. * load the functions that load the mixins into the prototype.
  15728. *
  15729. * @private
  15730. */
  15731. Graph.prototype._initializeMixinLoaders = function () {
  15732. for (var mixinFunction in graphMixinLoaders) {
  15733. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  15734. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  15735. }
  15736. }
  15737. };
  15738. /**
  15739. * Load the XY positions of the nodes into the dataset.
  15740. */
  15741. Graph.prototype.storePosition = function() {
  15742. var dataArray = [];
  15743. for (var nodeId in this.nodes) {
  15744. if (this.nodes.hasOwnProperty(nodeId)) {
  15745. var node = this.nodes[nodeId];
  15746. var allowedToMoveX = !this.nodes.xFixed;
  15747. var allowedToMoveY = !this.nodes.yFixed;
  15748. if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
  15749. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  15750. }
  15751. }
  15752. }
  15753. this.nodesData.update(dataArray);
  15754. };
  15755. /**
  15756. * Center a node in view.
  15757. *
  15758. * @param {Number} nodeId
  15759. * @param {Number} [zoomLevel]
  15760. */
  15761. Graph.prototype.focusOnNode = function (nodeId, zoomLevel) {
  15762. if (this.nodes.hasOwnProperty(nodeId)) {
  15763. if (zoomLevel === undefined) {
  15764. zoomLevel = this._getScale();
  15765. }
  15766. var nodePosition= {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y};
  15767. var requiredScale = zoomLevel;
  15768. this._setScale(requiredScale);
  15769. var canvasCenter = this.DOMtoCanvas({x:0.5 * this.frame.canvas.width,y:0.5 * this.frame.canvas.height});
  15770. var translation = this._getTranslation();
  15771. var distanceFromCenter = {x:canvasCenter.x - nodePosition.x,
  15772. y:canvasCenter.y - nodePosition.y};
  15773. this._setTranslation(translation.x + requiredScale * distanceFromCenter.x,
  15774. translation.y + requiredScale * distanceFromCenter.y);
  15775. this.redraw();
  15776. }
  15777. else {
  15778. console.log("This nodeId cannot be found.")
  15779. }
  15780. };
  15781. /**
  15782. * @constructor Graph3d
  15783. * The Graph is a visualization Graphs on a time line
  15784. *
  15785. * Graph is developed in javascript as a Google Visualization Chart.
  15786. *
  15787. * @param {Element} container The DOM element in which the Graph will
  15788. * be created. Normally a div element.
  15789. * @param {DataSet | DataView | Array} [data]
  15790. * @param {Object} [options]
  15791. */
  15792. function Graph3d(container, data, options) {
  15793. // create variables and set default values
  15794. this.containerElement = container;
  15795. this.width = '400px';
  15796. this.height = '400px';
  15797. this.margin = 10; // px
  15798. this.defaultXCenter = '55%';
  15799. this.defaultYCenter = '50%';
  15800. this.xLabel = 'x';
  15801. this.yLabel = 'y';
  15802. this.zLabel = 'z';
  15803. this.filterLabel = 'time';
  15804. this.legendLabel = 'value';
  15805. this.style = Graph3d.STYLE.DOT;
  15806. this.showPerspective = true;
  15807. this.showGrid = true;
  15808. this.keepAspectRatio = true;
  15809. this.showShadow = false;
  15810. this.showGrayBottom = false; // TODO: this does not work correctly
  15811. this.showTooltip = false;
  15812. this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
  15813. this.animationInterval = 1000; // milliseconds
  15814. this.animationPreload = false;
  15815. this.camera = new Graph3d.Camera();
  15816. this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
  15817. this.dataTable = null; // The original data table
  15818. this.dataPoints = null; // The table with point objects
  15819. // the column indexes
  15820. this.colX = undefined;
  15821. this.colY = undefined;
  15822. this.colZ = undefined;
  15823. this.colValue = undefined;
  15824. this.colFilter = undefined;
  15825. this.xMin = 0;
  15826. this.xStep = undefined; // auto by default
  15827. this.xMax = 1;
  15828. this.yMin = 0;
  15829. this.yStep = undefined; // auto by default
  15830. this.yMax = 1;
  15831. this.zMin = 0;
  15832. this.zStep = undefined; // auto by default
  15833. this.zMax = 1;
  15834. this.valueMin = 0;
  15835. this.valueMax = 1;
  15836. this.xBarWidth = 1;
  15837. this.yBarWidth = 1;
  15838. // TODO: customize axis range
  15839. // constants
  15840. this.colorAxis = '#4D4D4D';
  15841. this.colorGrid = '#D3D3D3';
  15842. this.colorDot = '#7DC1FF';
  15843. this.colorDotBorder = '#3267D2';
  15844. // create a frame and canvas
  15845. this.create();
  15846. // apply options (also when undefined)
  15847. this.setOptions(options);
  15848. // apply data
  15849. if (data) {
  15850. this.setData(data);
  15851. }
  15852. }
  15853. // Extend Graph with an Emitter mixin
  15854. Emitter(Graph3d.prototype);
  15855. /**
  15856. * @class Camera
  15857. * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
  15858. * The camera is always looking in the direction of the origin of the arm.
  15859. * This way, the camera always rotates around one fixed point, the location
  15860. * of the camera arm.
  15861. *
  15862. * Documentation:
  15863. * http://en.wikipedia.org/wiki/3D_projection
  15864. */
  15865. Graph3d.Camera = function () {
  15866. this.armLocation = new Point3d();
  15867. this.armRotation = {};
  15868. this.armRotation.horizontal = 0;
  15869. this.armRotation.vertical = 0;
  15870. this.armLength = 1.7;
  15871. this.cameraLocation = new Point3d();
  15872. this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
  15873. this.calculateCameraOrientation();
  15874. };
  15875. /**
  15876. * Set the location (origin) of the arm
  15877. * @param {Number} x Normalized value of x
  15878. * @param {Number} y Normalized value of y
  15879. * @param {Number} z Normalized value of z
  15880. */
  15881. Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
  15882. this.armLocation.x = x;
  15883. this.armLocation.y = y;
  15884. this.armLocation.z = z;
  15885. this.calculateCameraOrientation();
  15886. };
  15887. /**
  15888. * Set the rotation of the camera arm
  15889. * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
  15890. * Optional, can be left undefined.
  15891. * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
  15892. * if vertical=0.5*PI, the graph is shown from the
  15893. * top. Optional, can be left undefined.
  15894. */
  15895. Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
  15896. if (horizontal !== undefined) {
  15897. this.armRotation.horizontal = horizontal;
  15898. }
  15899. if (vertical !== undefined) {
  15900. this.armRotation.vertical = vertical;
  15901. if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
  15902. if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
  15903. }
  15904. if (horizontal !== undefined || vertical !== undefined) {
  15905. this.calculateCameraOrientation();
  15906. }
  15907. };
  15908. /**
  15909. * Retrieve the current arm rotation
  15910. * @return {object} An object with parameters horizontal and vertical
  15911. */
  15912. Graph3d.Camera.prototype.getArmRotation = function() {
  15913. var rot = {};
  15914. rot.horizontal = this.armRotation.horizontal;
  15915. rot.vertical = this.armRotation.vertical;
  15916. return rot;
  15917. };
  15918. /**
  15919. * Set the (normalized) length of the camera arm.
  15920. * @param {Number} length A length between 0.71 and 5.0
  15921. */
  15922. Graph3d.Camera.prototype.setArmLength = function(length) {
  15923. if (length === undefined)
  15924. return;
  15925. this.armLength = length;
  15926. // Radius must be larger than the corner of the graph,
  15927. // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
  15928. // graph
  15929. if (this.armLength < 0.71) this.armLength = 0.71;
  15930. if (this.armLength > 5.0) this.armLength = 5.0;
  15931. this.calculateCameraOrientation();
  15932. };
  15933. /**
  15934. * Retrieve the arm length
  15935. * @return {Number} length
  15936. */
  15937. Graph3d.Camera.prototype.getArmLength = function() {
  15938. return this.armLength;
  15939. };
  15940. /**
  15941. * Retrieve the camera location
  15942. * @return {Point3d} cameraLocation
  15943. */
  15944. Graph3d.Camera.prototype.getCameraLocation = function() {
  15945. return this.cameraLocation;
  15946. };
  15947. /**
  15948. * Retrieve the camera rotation
  15949. * @return {Point3d} cameraRotation
  15950. */
  15951. Graph3d.Camera.prototype.getCameraRotation = function() {
  15952. return this.cameraRotation;
  15953. };
  15954. /**
  15955. * Calculate the location and rotation of the camera based on the
  15956. * position and orientation of the camera arm
  15957. */
  15958. Graph3d.Camera.prototype.calculateCameraOrientation = function() {
  15959. // calculate location of the camera
  15960. this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  15961. this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  15962. this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
  15963. // calculate rotation of the camera
  15964. this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
  15965. this.cameraRotation.y = 0;
  15966. this.cameraRotation.z = -this.armRotation.horizontal;
  15967. };
  15968. /**
  15969. * Calculate the scaling values, dependent on the range in x, y, and z direction
  15970. */
  15971. Graph3d.prototype._setScale = function() {
  15972. this.scale = new Point3d(1 / (this.xMax - this.xMin),
  15973. 1 / (this.yMax - this.yMin),
  15974. 1 / (this.zMax - this.zMin));
  15975. // keep aspect ration between x and y scale if desired
  15976. if (this.keepAspectRatio) {
  15977. if (this.scale.x < this.scale.y) {
  15978. //noinspection JSSuspiciousNameCombination
  15979. this.scale.y = this.scale.x;
  15980. }
  15981. else {
  15982. //noinspection JSSuspiciousNameCombination
  15983. this.scale.x = this.scale.y;
  15984. }
  15985. }
  15986. // scale the vertical axis
  15987. this.scale.z *= this.verticalRatio;
  15988. // TODO: can this be automated? verticalRatio?
  15989. // determine scale for (optional) value
  15990. this.scale.value = 1 / (this.valueMax - this.valueMin);
  15991. // position the camera arm
  15992. var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
  15993. var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
  15994. var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
  15995. this.camera.setArmLocation(xCenter, yCenter, zCenter);
  15996. };
  15997. /**
  15998. * Convert a 3D location to a 2D location on screen
  15999. * http://en.wikipedia.org/wiki/3D_projection
  16000. * @param {Point3d} point3d A 3D point with parameters x, y, z
  16001. * @return {Point2d} point2d A 2D point with parameters x, y
  16002. */
  16003. Graph3d.prototype._convert3Dto2D = function(point3d) {
  16004. var translation = this._convertPointToTranslation(point3d);
  16005. return this._convertTranslationToScreen(translation);
  16006. };
  16007. /**
  16008. * Convert a 3D location its translation seen from the camera
  16009. * http://en.wikipedia.org/wiki/3D_projection
  16010. * @param {Point3d} point3d A 3D point with parameters x, y, z
  16011. * @return {Point3d} translation A 3D point with parameters x, y, z This is
  16012. * the translation of the point, seen from the
  16013. * camera
  16014. */
  16015. Graph3d.prototype._convertPointToTranslation = function(point3d) {
  16016. var ax = point3d.x * this.scale.x,
  16017. ay = point3d.y * this.scale.y,
  16018. az = point3d.z * this.scale.z,
  16019. cx = this.camera.getCameraLocation().x,
  16020. cy = this.camera.getCameraLocation().y,
  16021. cz = this.camera.getCameraLocation().z,
  16022. // calculate angles
  16023. sinTx = Math.sin(this.camera.getCameraRotation().x),
  16024. cosTx = Math.cos(this.camera.getCameraRotation().x),
  16025. sinTy = Math.sin(this.camera.getCameraRotation().y),
  16026. cosTy = Math.cos(this.camera.getCameraRotation().y),
  16027. sinTz = Math.sin(this.camera.getCameraRotation().z),
  16028. cosTz = Math.cos(this.camera.getCameraRotation().z),
  16029. // calculate translation
  16030. dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
  16031. dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
  16032. dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
  16033. return new Point3d(dx, dy, dz);
  16034. };
  16035. /**
  16036. * Convert a translation point to a point on the screen
  16037. * @param {Point3d} translation A 3D point with parameters x, y, z This is
  16038. * the translation of the point, seen from the
  16039. * camera
  16040. * @return {Point2d} point2d A 2D point with parameters x, y
  16041. */
  16042. Graph3d.prototype._convertTranslationToScreen = function(translation) {
  16043. var ex = this.eye.x,
  16044. ey = this.eye.y,
  16045. ez = this.eye.z,
  16046. dx = translation.x,
  16047. dy = translation.y,
  16048. dz = translation.z;
  16049. // calculate position on screen from translation
  16050. var bx;
  16051. var by;
  16052. if (this.showPerspective) {
  16053. bx = (dx - ex) * (ez / dz);
  16054. by = (dy - ey) * (ez / dz);
  16055. }
  16056. else {
  16057. bx = dx * -(ez / this.camera.getArmLength());
  16058. by = dy * -(ez / this.camera.getArmLength());
  16059. }
  16060. // shift and scale the point to the center of the screen
  16061. // use the width of the graph to scale both horizontally and vertically.
  16062. return new Point2d(
  16063. this.xcenter + bx * this.frame.canvas.clientWidth,
  16064. this.ycenter - by * this.frame.canvas.clientWidth);
  16065. };
  16066. /**
  16067. * Set the background styling for the graph
  16068. * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
  16069. */
  16070. Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
  16071. var fill = 'white';
  16072. var stroke = 'gray';
  16073. var strokeWidth = 1;
  16074. if (typeof(backgroundColor) === 'string') {
  16075. fill = backgroundColor;
  16076. stroke = 'none';
  16077. strokeWidth = 0;
  16078. }
  16079. else if (typeof(backgroundColor) === 'object') {
  16080. if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
  16081. if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
  16082. if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
  16083. }
  16084. else if (backgroundColor === undefined) {
  16085. // use use defaults
  16086. }
  16087. else {
  16088. throw 'Unsupported type of backgroundColor';
  16089. }
  16090. this.frame.style.backgroundColor = fill;
  16091. this.frame.style.borderColor = stroke;
  16092. this.frame.style.borderWidth = strokeWidth + 'px';
  16093. this.frame.style.borderStyle = 'solid';
  16094. };
  16095. /// enumerate the available styles
  16096. Graph3d.STYLE = {
  16097. BAR: 0,
  16098. BARCOLOR: 1,
  16099. BARSIZE: 2,
  16100. DOT : 3,
  16101. DOTLINE : 4,
  16102. DOTCOLOR: 5,
  16103. DOTSIZE: 6,
  16104. GRID : 7,
  16105. LINE: 8,
  16106. SURFACE : 9
  16107. };
  16108. /**
  16109. * Retrieve the style index from given styleName
  16110. * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
  16111. * @return {Number} styleNumber Enumeration value representing the style, or -1
  16112. * when not found
  16113. */
  16114. Graph3d.prototype._getStyleNumber = function(styleName) {
  16115. switch (styleName) {
  16116. case 'dot': return Graph3d.STYLE.DOT;
  16117. case 'dot-line': return Graph3d.STYLE.DOTLINE;
  16118. case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
  16119. case 'dot-size': return Graph3d.STYLE.DOTSIZE;
  16120. case 'line': return Graph3d.STYLE.LINE;
  16121. case 'grid': return Graph3d.STYLE.GRID;
  16122. case 'surface': return Graph3d.STYLE.SURFACE;
  16123. case 'bar': return Graph3d.STYLE.BAR;
  16124. case 'bar-color': return Graph3d.STYLE.BARCOLOR;
  16125. case 'bar-size': return Graph3d.STYLE.BARSIZE;
  16126. }
  16127. return -1;
  16128. };
  16129. /**
  16130. * Determine the indexes of the data columns, based on the given style and data
  16131. * @param {DataSet} data
  16132. * @param {Number} style
  16133. */
  16134. Graph3d.prototype._determineColumnIndexes = function(data, style) {
  16135. if (this.style === Graph3d.STYLE.DOT ||
  16136. this.style === Graph3d.STYLE.DOTLINE ||
  16137. this.style === Graph3d.STYLE.LINE ||
  16138. this.style === Graph3d.STYLE.GRID ||
  16139. this.style === Graph3d.STYLE.SURFACE ||
  16140. this.style === Graph3d.STYLE.BAR) {
  16141. // 3 columns expected, and optionally a 4th with filter values
  16142. this.colX = 0;
  16143. this.colY = 1;
  16144. this.colZ = 2;
  16145. this.colValue = undefined;
  16146. if (data.getNumberOfColumns() > 3) {
  16147. this.colFilter = 3;
  16148. }
  16149. }
  16150. else if (this.style === Graph3d.STYLE.DOTCOLOR ||
  16151. this.style === Graph3d.STYLE.DOTSIZE ||
  16152. this.style === Graph3d.STYLE.BARCOLOR ||
  16153. this.style === Graph3d.STYLE.BARSIZE) {
  16154. // 4 columns expected, and optionally a 5th with filter values
  16155. this.colX = 0;
  16156. this.colY = 1;
  16157. this.colZ = 2;
  16158. this.colValue = 3;
  16159. if (data.getNumberOfColumns() > 4) {
  16160. this.colFilter = 4;
  16161. }
  16162. }
  16163. else {
  16164. throw 'Unknown style "' + this.style + '"';
  16165. }
  16166. };
  16167. Graph3d.prototype.getNumberOfRows = function(data) {
  16168. return data.length;
  16169. }
  16170. Graph3d.prototype.getNumberOfColumns = function(data) {
  16171. var counter = 0;
  16172. for (var column in data[0]) {
  16173. if (data[0].hasOwnProperty(column)) {
  16174. counter++;
  16175. }
  16176. }
  16177. return counter;
  16178. }
  16179. Graph3d.prototype.getDistinctValues = function(data, column) {
  16180. var distinctValues = [];
  16181. for (var i = 0; i < data.length; i++) {
  16182. if (distinctValues.indexOf(data[i][column]) == -1) {
  16183. distinctValues.push(data[i][column]);
  16184. }
  16185. }
  16186. return distinctValues;
  16187. }
  16188. Graph3d.prototype.getColumnRange = function(data,column) {
  16189. var minMax = {min:data[0][column],max:data[0][column]};
  16190. for (var i = 0; i < data.length; i++) {
  16191. if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
  16192. if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
  16193. }
  16194. return minMax;
  16195. };
  16196. /**
  16197. * Initialize the data from the data table. Calculate minimum and maximum values
  16198. * and column index values
  16199. * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
  16200. * @param {Number} style Style Number
  16201. */
  16202. Graph3d.prototype._dataInitialize = function (rawData, style) {
  16203. var me = this;
  16204. // unsubscribe from the dataTable
  16205. if (this.dataSet) {
  16206. this.dataSet.off('*', this._onChange);
  16207. }
  16208. if (rawData === undefined)
  16209. return;
  16210. if (Array.isArray(rawData)) {
  16211. rawData = new DataSet(rawData);
  16212. }
  16213. var data;
  16214. if (rawData instanceof DataSet || rawData instanceof DataView) {
  16215. data = rawData.get();
  16216. }
  16217. else {
  16218. throw new Error('Array, DataSet, or DataView expected');
  16219. }
  16220. if (data.length == 0)
  16221. return;
  16222. this.dataSet = rawData;
  16223. this.dataTable = data;
  16224. // subscribe to changes in the dataset
  16225. this._onChange = function () {
  16226. me.setData(me.dataSet);
  16227. };
  16228. this.dataSet.on('*', this._onChange);
  16229. // _determineColumnIndexes
  16230. // getNumberOfRows (points)
  16231. // getNumberOfColumns (x,y,z,v,t,t1,t2...)
  16232. // getDistinctValues (unique values?)
  16233. // getColumnRange
  16234. // determine the location of x,y,z,value,filter columns
  16235. this.colX = 'x';
  16236. this.colY = 'y';
  16237. this.colZ = 'z';
  16238. this.colValue = 'style';
  16239. this.colFilter = 'filter';
  16240. // check if a filter column is provided
  16241. if (data[0].hasOwnProperty('filter')) {
  16242. if (this.dataFilter === undefined) {
  16243. this.dataFilter = new Filter(rawData, this.colFilter, this);
  16244. this.dataFilter.setOnLoadCallback(function() {me.redraw();});
  16245. }
  16246. }
  16247. var withBars = this.style == Graph3d.STYLE.BAR ||
  16248. this.style == Graph3d.STYLE.BARCOLOR ||
  16249. this.style == Graph3d.STYLE.BARSIZE;
  16250. // determine barWidth from data
  16251. if (withBars) {
  16252. if (this.defaultXBarWidth !== undefined) {
  16253. this.xBarWidth = this.defaultXBarWidth;
  16254. }
  16255. else {
  16256. var dataX = this.getDistinctValues(data,this.colX);
  16257. this.xBarWidth = (dataX[1] - dataX[0]) || 1;
  16258. }
  16259. if (this.defaultYBarWidth !== undefined) {
  16260. this.yBarWidth = this.defaultYBarWidth;
  16261. }
  16262. else {
  16263. var dataY = this.getDistinctValues(data,this.colY);
  16264. this.yBarWidth = (dataY[1] - dataY[0]) || 1;
  16265. }
  16266. }
  16267. // calculate minimums and maximums
  16268. var xRange = this.getColumnRange(data,this.colX);
  16269. if (withBars) {
  16270. xRange.min -= this.xBarWidth / 2;
  16271. xRange.max += this.xBarWidth / 2;
  16272. }
  16273. this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
  16274. this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
  16275. if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
  16276. this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
  16277. var yRange = this.getColumnRange(data,this.colY);
  16278. if (withBars) {
  16279. yRange.min -= this.yBarWidth / 2;
  16280. yRange.max += this.yBarWidth / 2;
  16281. }
  16282. this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
  16283. this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
  16284. if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
  16285. this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
  16286. var zRange = this.getColumnRange(data,this.colZ);
  16287. this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
  16288. this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
  16289. if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
  16290. this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
  16291. if (this.colValue !== undefined) {
  16292. var valueRange = this.getColumnRange(data,this.colValue);
  16293. this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
  16294. this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
  16295. if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
  16296. }
  16297. // set the scale dependent on the ranges.
  16298. this._setScale();
  16299. };
  16300. /**
  16301. * Filter the data based on the current filter
  16302. * @param {Array} data
  16303. * @return {Array} dataPoints Array with point objects which can be drawn on screen
  16304. */
  16305. Graph3d.prototype._getDataPoints = function (data) {
  16306. // TODO: store the created matrix dataPoints in the filters instead of reloading each time
  16307. var x, y, i, z, obj, point;
  16308. var dataPoints = [];
  16309. if (this.style === Graph3d.STYLE.GRID ||
  16310. this.style === Graph3d.STYLE.SURFACE) {
  16311. // copy all values from the google data table to a matrix
  16312. // the provided values are supposed to form a grid of (x,y) positions
  16313. // create two lists with all present x and y values
  16314. var dataX = [];
  16315. var dataY = [];
  16316. for (i = 0; i < this.getNumberOfRows(data); i++) {
  16317. x = data[i][this.colX] || 0;
  16318. y = data[i][this.colY] || 0;
  16319. if (dataX.indexOf(x) === -1) {
  16320. dataX.push(x);
  16321. }
  16322. if (dataY.indexOf(y) === -1) {
  16323. dataY.push(y);
  16324. }
  16325. }
  16326. function sortNumber(a, b) {
  16327. return a - b;
  16328. }
  16329. dataX.sort(sortNumber);
  16330. dataY.sort(sortNumber);
  16331. // create a grid, a 2d matrix, with all values.
  16332. var dataMatrix = []; // temporary data matrix
  16333. for (i = 0; i < data.length; i++) {
  16334. x = data[i][this.colX] || 0;
  16335. y = data[i][this.colY] || 0;
  16336. z = data[i][this.colZ] || 0;
  16337. var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
  16338. var yIndex = dataY.indexOf(y);
  16339. if (dataMatrix[xIndex] === undefined) {
  16340. dataMatrix[xIndex] = [];
  16341. }
  16342. var point3d = new Point3d();
  16343. point3d.x = x;
  16344. point3d.y = y;
  16345. point3d.z = z;
  16346. obj = {};
  16347. obj.point = point3d;
  16348. obj.trans = undefined;
  16349. obj.screen = undefined;
  16350. obj.bottom = new Point3d(x, y, this.zMin);
  16351. dataMatrix[xIndex][yIndex] = obj;
  16352. dataPoints.push(obj);
  16353. }
  16354. // fill in the pointers to the neighbors.
  16355. for (x = 0; x < dataMatrix.length; x++) {
  16356. for (y = 0; y < dataMatrix[x].length; y++) {
  16357. if (dataMatrix[x][y]) {
  16358. dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
  16359. dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
  16360. dataMatrix[x][y].pointCross =
  16361. (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
  16362. dataMatrix[x+1][y+1] :
  16363. undefined;
  16364. }
  16365. }
  16366. }
  16367. }
  16368. else { // 'dot', 'dot-line', etc.
  16369. // copy all values from the google data table to a list with Point3d objects
  16370. for (i = 0; i < data.length; i++) {
  16371. point = new Point3d();
  16372. point.x = data[i][this.colX] || 0;
  16373. point.y = data[i][this.colY] || 0;
  16374. point.z = data[i][this.colZ] || 0;
  16375. if (this.colValue !== undefined) {
  16376. point.value = data[i][this.colValue] || 0;
  16377. }
  16378. obj = {};
  16379. obj.point = point;
  16380. obj.bottom = new Point3d(point.x, point.y, this.zMin);
  16381. obj.trans = undefined;
  16382. obj.screen = undefined;
  16383. dataPoints.push(obj);
  16384. }
  16385. }
  16386. return dataPoints;
  16387. };
  16388. /**
  16389. * Append suffix 'px' to provided value x
  16390. * @param {int} x An integer value
  16391. * @return {string} the string value of x, followed by the suffix 'px'
  16392. */
  16393. Graph3d.px = function(x) {
  16394. return x + 'px';
  16395. };
  16396. /**
  16397. * Create the main frame for the Graph3d.
  16398. * This function is executed once when a Graph3d object is created. The frame
  16399. * contains a canvas, and this canvas contains all objects like the axis and
  16400. * nodes.
  16401. */
  16402. Graph3d.prototype.create = function () {
  16403. // remove all elements from the container element.
  16404. while (this.containerElement.hasChildNodes()) {
  16405. this.containerElement.removeChild(this.containerElement.firstChild);
  16406. }
  16407. this.frame = document.createElement('div');
  16408. this.frame.style.position = 'relative';
  16409. this.frame.style.overflow = 'hidden';
  16410. // create the graph canvas (HTML canvas element)
  16411. this.frame.canvas = document.createElement( 'canvas' );
  16412. this.frame.canvas.style.position = 'relative';
  16413. this.frame.appendChild(this.frame.canvas);
  16414. //if (!this.frame.canvas.getContext) {
  16415. {
  16416. var noCanvas = document.createElement( 'DIV' );
  16417. noCanvas.style.color = 'red';
  16418. noCanvas.style.fontWeight = 'bold' ;
  16419. noCanvas.style.padding = '10px';
  16420. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  16421. this.frame.canvas.appendChild(noCanvas);
  16422. }
  16423. this.frame.filter = document.createElement( 'div' );
  16424. this.frame.filter.style.position = 'absolute';
  16425. this.frame.filter.style.bottom = '0px';
  16426. this.frame.filter.style.left = '0px';
  16427. this.frame.filter.style.width = '100%';
  16428. this.frame.appendChild(this.frame.filter);
  16429. // add event listeners to handle moving and zooming the contents
  16430. var me = this;
  16431. var onmousedown = function (event) {me._onMouseDown(event);};
  16432. var ontouchstart = function (event) {me._onTouchStart(event);};
  16433. var onmousewheel = function (event) {me._onWheel(event);};
  16434. var ontooltip = function (event) {me._onTooltip(event);};
  16435. // TODO: these events are never cleaned up... can give a 'memory leakage'
  16436. G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown);
  16437. G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown);
  16438. G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart);
  16439. G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
  16440. G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip);
  16441. // add the new graph to the container element
  16442. this.containerElement.appendChild(this.frame);
  16443. };
  16444. /**
  16445. * Set a new size for the graph
  16446. * @param {string} width Width in pixels or percentage (for example '800px'
  16447. * or '50%')
  16448. * @param {string} height Height in pixels or percentage (for example '400px'
  16449. * or '30%')
  16450. */
  16451. Graph3d.prototype.setSize = function(width, height) {
  16452. this.frame.style.width = width;
  16453. this.frame.style.height = height;
  16454. this._resizeCanvas();
  16455. };
  16456. /**
  16457. * Resize the canvas to the current size of the frame
  16458. */
  16459. Graph3d.prototype._resizeCanvas = function() {
  16460. this.frame.canvas.style.width = '100%';
  16461. this.frame.canvas.style.height = '100%';
  16462. this.frame.canvas.width = this.frame.canvas.clientWidth;
  16463. this.frame.canvas.height = this.frame.canvas.clientHeight;
  16464. // adjust with for margin
  16465. this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
  16466. };
  16467. /**
  16468. * Start animation
  16469. */
  16470. Graph3d.prototype.animationStart = function() {
  16471. if (!this.frame.filter || !this.frame.filter.slider)
  16472. throw 'No animation available';
  16473. this.frame.filter.slider.play();
  16474. };
  16475. /**
  16476. * Stop animation
  16477. */
  16478. Graph3d.prototype.animationStop = function() {
  16479. if (!this.frame.filter || !this.frame.filter.slider) return;
  16480. this.frame.filter.slider.stop();
  16481. };
  16482. /**
  16483. * Resize the center position based on the current values in this.defaultXCenter
  16484. * and this.defaultYCenter (which are strings with a percentage or a value
  16485. * in pixels). The center positions are the variables this.xCenter
  16486. * and this.yCenter
  16487. */
  16488. Graph3d.prototype._resizeCenter = function() {
  16489. // calculate the horizontal center position
  16490. if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
  16491. this.xcenter =
  16492. parseFloat(this.defaultXCenter) / 100 *
  16493. this.frame.canvas.clientWidth;
  16494. }
  16495. else {
  16496. this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
  16497. }
  16498. // calculate the vertical center position
  16499. if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
  16500. this.ycenter =
  16501. parseFloat(this.defaultYCenter) / 100 *
  16502. (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
  16503. }
  16504. else {
  16505. this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
  16506. }
  16507. };
  16508. /**
  16509. * Set the rotation and distance of the camera
  16510. * @param {Object} pos An object with the camera position. The object
  16511. * contains three parameters:
  16512. * - horizontal {Number}
  16513. * The horizontal rotation, between 0 and 2*PI.
  16514. * Optional, can be left undefined.
  16515. * - vertical {Number}
  16516. * The vertical rotation, between 0 and 0.5*PI
  16517. * if vertical=0.5*PI, the graph is shown from the
  16518. * top. Optional, can be left undefined.
  16519. * - distance {Number}
  16520. * The (normalized) distance of the camera to the
  16521. * center of the graph, a value between 0.71 and 5.0.
  16522. * Optional, can be left undefined.
  16523. */
  16524. Graph3d.prototype.setCameraPosition = function(pos) {
  16525. if (pos === undefined) {
  16526. return;
  16527. }
  16528. if (pos.horizontal !== undefined && pos.vertical !== undefined) {
  16529. this.camera.setArmRotation(pos.horizontal, pos.vertical);
  16530. }
  16531. if (pos.distance !== undefined) {
  16532. this.camera.setArmLength(pos.distance);
  16533. }
  16534. this.redraw();
  16535. };
  16536. /**
  16537. * Retrieve the current camera rotation
  16538. * @return {object} An object with parameters horizontal, vertical, and
  16539. * distance
  16540. */
  16541. Graph3d.prototype.getCameraPosition = function() {
  16542. var pos = this.camera.getArmRotation();
  16543. pos.distance = this.camera.getArmLength();
  16544. return pos;
  16545. };
  16546. /**
  16547. * Load data into the 3D Graph
  16548. */
  16549. Graph3d.prototype._readData = function(data) {
  16550. // read the data
  16551. this._dataInitialize(data, this.style);
  16552. if (this.dataFilter) {
  16553. // apply filtering
  16554. this.dataPoints = this.dataFilter._getDataPoints();
  16555. }
  16556. else {
  16557. // no filtering. load all data
  16558. this.dataPoints = this._getDataPoints(this.dataTable);
  16559. }
  16560. // draw the filter
  16561. this._redrawFilter();
  16562. };
  16563. /**
  16564. * Replace the dataset of the Graph3d
  16565. * @param {Array | DataSet | DataView} data
  16566. */
  16567. Graph3d.prototype.setData = function (data) {
  16568. this._readData(data);
  16569. this.redraw();
  16570. // start animation when option is true
  16571. if (this.animationAutoStart && this.dataFilter) {
  16572. this.animationStart();
  16573. }
  16574. };
  16575. /**
  16576. * Update the options. Options will be merged with current options
  16577. * @param {Object} options
  16578. */
  16579. Graph3d.prototype.setOptions = function (options) {
  16580. var cameraPosition = undefined;
  16581. this.animationStop();
  16582. if (options !== undefined) {
  16583. // retrieve parameter values
  16584. if (options.width !== undefined) this.width = options.width;
  16585. if (options.height !== undefined) this.height = options.height;
  16586. if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
  16587. if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
  16588. if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
  16589. if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
  16590. if (options.xLabel !== undefined) this.xLabel = options.xLabel;
  16591. if (options.yLabel !== undefined) this.yLabel = options.yLabel;
  16592. if (options.zLabel !== undefined) this.zLabel = options.zLabel;
  16593. if (options.style !== undefined) {
  16594. var styleNumber = this._getStyleNumber(options.style);
  16595. if (styleNumber !== -1) {
  16596. this.style = styleNumber;
  16597. }
  16598. }
  16599. if (options.showGrid !== undefined) this.showGrid = options.showGrid;
  16600. if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
  16601. if (options.showShadow !== undefined) this.showShadow = options.showShadow;
  16602. if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
  16603. if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
  16604. if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
  16605. if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
  16606. if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
  16607. if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
  16608. if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
  16609. if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
  16610. if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
  16611. if (options.xMin !== undefined) this.defaultXMin = options.xMin;
  16612. if (options.xStep !== undefined) this.defaultXStep = options.xStep;
  16613. if (options.xMax !== undefined) this.defaultXMax = options.xMax;
  16614. if (options.yMin !== undefined) this.defaultYMin = options.yMin;
  16615. if (options.yStep !== undefined) this.defaultYStep = options.yStep;
  16616. if (options.yMax !== undefined) this.defaultYMax = options.yMax;
  16617. if (options.zMin !== undefined) this.defaultZMin = options.zMin;
  16618. if (options.zStep !== undefined) this.defaultZStep = options.zStep;
  16619. if (options.zMax !== undefined) this.defaultZMax = options.zMax;
  16620. if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
  16621. if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
  16622. if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
  16623. if (cameraPosition !== undefined) {
  16624. this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
  16625. this.camera.setArmLength(cameraPosition.distance);
  16626. }
  16627. else {
  16628. this.camera.setArmRotation(1.0, 0.5);
  16629. this.camera.setArmLength(1.7);
  16630. }
  16631. }
  16632. this._setBackgroundColor(options && options.backgroundColor);
  16633. this.setSize(this.width, this.height);
  16634. // re-load the data
  16635. if (this.dataTable) {
  16636. this.setData(this.dataTable);
  16637. }
  16638. // start animation when option is true
  16639. if (this.animationAutoStart && this.dataFilter) {
  16640. this.animationStart();
  16641. }
  16642. };
  16643. /**
  16644. * Redraw the Graph.
  16645. */
  16646. Graph3d.prototype.redraw = function() {
  16647. if (this.dataPoints === undefined) {
  16648. throw 'Error: graph data not initialized';
  16649. }
  16650. this._resizeCanvas();
  16651. this._resizeCenter();
  16652. this._redrawSlider();
  16653. this._redrawClear();
  16654. this._redrawAxis();
  16655. if (this.style === Graph3d.STYLE.GRID ||
  16656. this.style === Graph3d.STYLE.SURFACE) {
  16657. this._redrawDataGrid();
  16658. }
  16659. else if (this.style === Graph3d.STYLE.LINE) {
  16660. this._redrawDataLine();
  16661. }
  16662. else if (this.style === Graph3d.STYLE.BAR ||
  16663. this.style === Graph3d.STYLE.BARCOLOR ||
  16664. this.style === Graph3d.STYLE.BARSIZE) {
  16665. this._redrawDataBar();
  16666. }
  16667. else {
  16668. // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
  16669. this._redrawDataDot();
  16670. }
  16671. this._redrawInfo();
  16672. this._redrawLegend();
  16673. };
  16674. /**
  16675. * Clear the canvas before redrawing
  16676. */
  16677. Graph3d.prototype._redrawClear = function() {
  16678. var canvas = this.frame.canvas;
  16679. var ctx = canvas.getContext('2d');
  16680. ctx.clearRect(0, 0, canvas.width, canvas.height);
  16681. };
  16682. /**
  16683. * Redraw the legend showing the colors
  16684. */
  16685. Graph3d.prototype._redrawLegend = function() {
  16686. var y;
  16687. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  16688. this.style === Graph3d.STYLE.DOTSIZE) {
  16689. var dotSize = this.frame.clientWidth * 0.02;
  16690. var widthMin, widthMax;
  16691. if (this.style === Graph3d.STYLE.DOTSIZE) {
  16692. widthMin = dotSize / 2; // px
  16693. widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
  16694. }
  16695. else {
  16696. widthMin = 20; // px
  16697. widthMax = 20; // px
  16698. }
  16699. var height = Math.max(this.frame.clientHeight * 0.25, 100);
  16700. var top = this.margin;
  16701. var right = this.frame.clientWidth - this.margin;
  16702. var left = right - widthMax;
  16703. var bottom = top + height;
  16704. }
  16705. var canvas = this.frame.canvas;
  16706. var ctx = canvas.getContext('2d');
  16707. ctx.lineWidth = 1;
  16708. ctx.font = '14px arial'; // TODO: put in options
  16709. if (this.style === Graph3d.STYLE.DOTCOLOR) {
  16710. // draw the color bar
  16711. var ymin = 0;
  16712. var ymax = height; // Todo: make height customizable
  16713. for (y = ymin; y < ymax; y++) {
  16714. var f = (y - ymin) / (ymax - ymin);
  16715. //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
  16716. var hue = f * 240;
  16717. var color = this._hsv2rgb(hue, 1, 1);
  16718. ctx.strokeStyle = color;
  16719. ctx.beginPath();
  16720. ctx.moveTo(left, top + y);
  16721. ctx.lineTo(right, top + y);
  16722. ctx.stroke();
  16723. }
  16724. ctx.strokeStyle = this.colorAxis;
  16725. ctx.strokeRect(left, top, widthMax, height);
  16726. }
  16727. if (this.style === Graph3d.STYLE.DOTSIZE) {
  16728. // draw border around color bar
  16729. ctx.strokeStyle = this.colorAxis;
  16730. ctx.fillStyle = this.colorDot;
  16731. ctx.beginPath();
  16732. ctx.moveTo(left, top);
  16733. ctx.lineTo(right, top);
  16734. ctx.lineTo(right - widthMax + widthMin, bottom);
  16735. ctx.lineTo(left, bottom);
  16736. ctx.closePath();
  16737. ctx.fill();
  16738. ctx.stroke();
  16739. }
  16740. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  16741. this.style === Graph3d.STYLE.DOTSIZE) {
  16742. // print values along the color bar
  16743. var gridLineLen = 5; // px
  16744. var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
  16745. step.start();
  16746. if (step.getCurrent() < this.valueMin) {
  16747. step.next();
  16748. }
  16749. while (!step.end()) {
  16750. y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
  16751. ctx.beginPath();
  16752. ctx.moveTo(left - gridLineLen, y);
  16753. ctx.lineTo(left, y);
  16754. ctx.stroke();
  16755. ctx.textAlign = 'right';
  16756. ctx.textBaseline = 'middle';
  16757. ctx.fillStyle = this.colorAxis;
  16758. ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
  16759. step.next();
  16760. }
  16761. ctx.textAlign = 'right';
  16762. ctx.textBaseline = 'top';
  16763. var label = this.legendLabel;
  16764. ctx.fillText(label, right, bottom + this.margin);
  16765. }
  16766. };
  16767. /**
  16768. * Redraw the filter
  16769. */
  16770. Graph3d.prototype._redrawFilter = function() {
  16771. this.frame.filter.innerHTML = '';
  16772. if (this.dataFilter) {
  16773. var options = {
  16774. 'visible': this.showAnimationControls
  16775. };
  16776. var slider = new Slider(this.frame.filter, options);
  16777. this.frame.filter.slider = slider;
  16778. // TODO: css here is not nice here...
  16779. this.frame.filter.style.padding = '10px';
  16780. //this.frame.filter.style.backgroundColor = '#EFEFEF';
  16781. slider.setValues(this.dataFilter.values);
  16782. slider.setPlayInterval(this.animationInterval);
  16783. // create an event handler
  16784. var me = this;
  16785. var onchange = function () {
  16786. var index = slider.getIndex();
  16787. me.dataFilter.selectValue(index);
  16788. me.dataPoints = me.dataFilter._getDataPoints();
  16789. me.redraw();
  16790. };
  16791. slider.setOnChangeCallback(onchange);
  16792. }
  16793. else {
  16794. this.frame.filter.slider = undefined;
  16795. }
  16796. };
  16797. /**
  16798. * Redraw the slider
  16799. */
  16800. Graph3d.prototype._redrawSlider = function() {
  16801. if ( this.frame.filter.slider !== undefined) {
  16802. this.frame.filter.slider.redraw();
  16803. }
  16804. };
  16805. /**
  16806. * Redraw common information
  16807. */
  16808. Graph3d.prototype._redrawInfo = function() {
  16809. if (this.dataFilter) {
  16810. var canvas = this.frame.canvas;
  16811. var ctx = canvas.getContext('2d');
  16812. ctx.font = '14px arial'; // TODO: put in options
  16813. ctx.lineStyle = 'gray';
  16814. ctx.fillStyle = 'gray';
  16815. ctx.textAlign = 'left';
  16816. ctx.textBaseline = 'top';
  16817. var x = this.margin;
  16818. var y = this.margin;
  16819. ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
  16820. }
  16821. };
  16822. /**
  16823. * Redraw the axis
  16824. */
  16825. Graph3d.prototype._redrawAxis = function() {
  16826. var canvas = this.frame.canvas,
  16827. ctx = canvas.getContext('2d'),
  16828. from, to, step, prettyStep,
  16829. text, xText, yText, zText,
  16830. offset, xOffset, yOffset,
  16831. xMin2d, xMax2d;
  16832. // TODO: get the actual rendered style of the containerElement
  16833. //ctx.font = this.containerElement.style.font;
  16834. ctx.font = 24 / this.camera.getArmLength() + 'px arial';
  16835. // calculate the length for the short grid lines
  16836. var gridLenX = 0.025 / this.scale.x;
  16837. var gridLenY = 0.025 / this.scale.y;
  16838. var textMargin = 5 / this.camera.getArmLength(); // px
  16839. var armAngle = this.camera.getArmRotation().horizontal;
  16840. // draw x-grid lines
  16841. ctx.lineWidth = 1;
  16842. prettyStep = (this.defaultXStep === undefined);
  16843. step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
  16844. step.start();
  16845. if (step.getCurrent() < this.xMin) {
  16846. step.next();
  16847. }
  16848. while (!step.end()) {
  16849. var x = step.getCurrent();
  16850. if (this.showGrid) {
  16851. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  16852. to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  16853. ctx.strokeStyle = this.colorGrid;
  16854. ctx.beginPath();
  16855. ctx.moveTo(from.x, from.y);
  16856. ctx.lineTo(to.x, to.y);
  16857. ctx.stroke();
  16858. }
  16859. else {
  16860. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  16861. to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
  16862. ctx.strokeStyle = this.colorAxis;
  16863. ctx.beginPath();
  16864. ctx.moveTo(from.x, from.y);
  16865. ctx.lineTo(to.x, to.y);
  16866. ctx.stroke();
  16867. from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  16868. to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
  16869. ctx.strokeStyle = this.colorAxis;
  16870. ctx.beginPath();
  16871. ctx.moveTo(from.x, from.y);
  16872. ctx.lineTo(to.x, to.y);
  16873. ctx.stroke();
  16874. }
  16875. yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
  16876. text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
  16877. if (Math.cos(armAngle * 2) > 0) {
  16878. ctx.textAlign = 'center';
  16879. ctx.textBaseline = 'top';
  16880. text.y += textMargin;
  16881. }
  16882. else if (Math.sin(armAngle * 2) < 0){
  16883. ctx.textAlign = 'right';
  16884. ctx.textBaseline = 'middle';
  16885. }
  16886. else {
  16887. ctx.textAlign = 'left';
  16888. ctx.textBaseline = 'middle';
  16889. }
  16890. ctx.fillStyle = this.colorAxis;
  16891. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  16892. step.next();
  16893. }
  16894. // draw y-grid lines
  16895. ctx.lineWidth = 1;
  16896. prettyStep = (this.defaultYStep === undefined);
  16897. step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
  16898. step.start();
  16899. if (step.getCurrent() < this.yMin) {
  16900. step.next();
  16901. }
  16902. while (!step.end()) {
  16903. if (this.showGrid) {
  16904. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  16905. to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  16906. ctx.strokeStyle = this.colorGrid;
  16907. ctx.beginPath();
  16908. ctx.moveTo(from.x, from.y);
  16909. ctx.lineTo(to.x, to.y);
  16910. ctx.stroke();
  16911. }
  16912. else {
  16913. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  16914. to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
  16915. ctx.strokeStyle = this.colorAxis;
  16916. ctx.beginPath();
  16917. ctx.moveTo(from.x, from.y);
  16918. ctx.lineTo(to.x, to.y);
  16919. ctx.stroke();
  16920. from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  16921. to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
  16922. ctx.strokeStyle = this.colorAxis;
  16923. ctx.beginPath();
  16924. ctx.moveTo(from.x, from.y);
  16925. ctx.lineTo(to.x, to.y);
  16926. ctx.stroke();
  16927. }
  16928. xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
  16929. text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
  16930. if (Math.cos(armAngle * 2) < 0) {
  16931. ctx.textAlign = 'center';
  16932. ctx.textBaseline = 'top';
  16933. text.y += textMargin;
  16934. }
  16935. else if (Math.sin(armAngle * 2) > 0){
  16936. ctx.textAlign = 'right';
  16937. ctx.textBaseline = 'middle';
  16938. }
  16939. else {
  16940. ctx.textAlign = 'left';
  16941. ctx.textBaseline = 'middle';
  16942. }
  16943. ctx.fillStyle = this.colorAxis;
  16944. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  16945. step.next();
  16946. }
  16947. // draw z-grid lines and axis
  16948. ctx.lineWidth = 1;
  16949. prettyStep = (this.defaultZStep === undefined);
  16950. step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
  16951. step.start();
  16952. if (step.getCurrent() < this.zMin) {
  16953. step.next();
  16954. }
  16955. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  16956. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  16957. while (!step.end()) {
  16958. // TODO: make z-grid lines really 3d?
  16959. from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
  16960. ctx.strokeStyle = this.colorAxis;
  16961. ctx.beginPath();
  16962. ctx.moveTo(from.x, from.y);
  16963. ctx.lineTo(from.x - textMargin, from.y);
  16964. ctx.stroke();
  16965. ctx.textAlign = 'right';
  16966. ctx.textBaseline = 'middle';
  16967. ctx.fillStyle = this.colorAxis;
  16968. ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
  16969. step.next();
  16970. }
  16971. ctx.lineWidth = 1;
  16972. from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  16973. to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
  16974. ctx.strokeStyle = this.colorAxis;
  16975. ctx.beginPath();
  16976. ctx.moveTo(from.x, from.y);
  16977. ctx.lineTo(to.x, to.y);
  16978. ctx.stroke();
  16979. // draw x-axis
  16980. ctx.lineWidth = 1;
  16981. // line at yMin
  16982. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  16983. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  16984. ctx.strokeStyle = this.colorAxis;
  16985. ctx.beginPath();
  16986. ctx.moveTo(xMin2d.x, xMin2d.y);
  16987. ctx.lineTo(xMax2d.x, xMax2d.y);
  16988. ctx.stroke();
  16989. // line at ymax
  16990. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  16991. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  16992. ctx.strokeStyle = this.colorAxis;
  16993. ctx.beginPath();
  16994. ctx.moveTo(xMin2d.x, xMin2d.y);
  16995. ctx.lineTo(xMax2d.x, xMax2d.y);
  16996. ctx.stroke();
  16997. // draw y-axis
  16998. ctx.lineWidth = 1;
  16999. // line at xMin
  17000. from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  17001. to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  17002. ctx.strokeStyle = this.colorAxis;
  17003. ctx.beginPath();
  17004. ctx.moveTo(from.x, from.y);
  17005. ctx.lineTo(to.x, to.y);
  17006. ctx.stroke();
  17007. // line at xMax
  17008. from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  17009. to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  17010. ctx.strokeStyle = this.colorAxis;
  17011. ctx.beginPath();
  17012. ctx.moveTo(from.x, from.y);
  17013. ctx.lineTo(to.x, to.y);
  17014. ctx.stroke();
  17015. // draw x-label
  17016. var xLabel = this.xLabel;
  17017. if (xLabel.length > 0) {
  17018. yOffset = 0.1 / this.scale.y;
  17019. xText = (this.xMin + this.xMax) / 2;
  17020. yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
  17021. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  17022. if (Math.cos(armAngle * 2) > 0) {
  17023. ctx.textAlign = 'center';
  17024. ctx.textBaseline = 'top';
  17025. }
  17026. else if (Math.sin(armAngle * 2) < 0){
  17027. ctx.textAlign = 'right';
  17028. ctx.textBaseline = 'middle';
  17029. }
  17030. else {
  17031. ctx.textAlign = 'left';
  17032. ctx.textBaseline = 'middle';
  17033. }
  17034. ctx.fillStyle = this.colorAxis;
  17035. ctx.fillText(xLabel, text.x, text.y);
  17036. }
  17037. // draw y-label
  17038. var yLabel = this.yLabel;
  17039. if (yLabel.length > 0) {
  17040. xOffset = 0.1 / this.scale.x;
  17041. xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
  17042. yText = (this.yMin + this.yMax) / 2;
  17043. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  17044. if (Math.cos(armAngle * 2) < 0) {
  17045. ctx.textAlign = 'center';
  17046. ctx.textBaseline = 'top';
  17047. }
  17048. else if (Math.sin(armAngle * 2) > 0){
  17049. ctx.textAlign = 'right';
  17050. ctx.textBaseline = 'middle';
  17051. }
  17052. else {
  17053. ctx.textAlign = 'left';
  17054. ctx.textBaseline = 'middle';
  17055. }
  17056. ctx.fillStyle = this.colorAxis;
  17057. ctx.fillText(yLabel, text.x, text.y);
  17058. }
  17059. // draw z-label
  17060. var zLabel = this.zLabel;
  17061. if (zLabel.length > 0) {
  17062. offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
  17063. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  17064. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  17065. zText = (this.zMin + this.zMax) / 2;
  17066. text = this._convert3Dto2D(new Point3d(xText, yText, zText));
  17067. ctx.textAlign = 'right';
  17068. ctx.textBaseline = 'middle';
  17069. ctx.fillStyle = this.colorAxis;
  17070. ctx.fillText(zLabel, text.x - offset, text.y);
  17071. }
  17072. };
  17073. /**
  17074. * Calculate the color based on the given value.
  17075. * @param {Number} H Hue, a value be between 0 and 360
  17076. * @param {Number} S Saturation, a value between 0 and 1
  17077. * @param {Number} V Value, a value between 0 and 1
  17078. */
  17079. Graph3d.prototype._hsv2rgb = function(H, S, V) {
  17080. var R, G, B, C, Hi, X;
  17081. C = V * S;
  17082. Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
  17083. X = C * (1 - Math.abs(((H/60) % 2) - 1));
  17084. switch (Hi) {
  17085. case 0: R = C; G = X; B = 0; break;
  17086. case 1: R = X; G = C; B = 0; break;
  17087. case 2: R = 0; G = C; B = X; break;
  17088. case 3: R = 0; G = X; B = C; break;
  17089. case 4: R = X; G = 0; B = C; break;
  17090. case 5: R = C; G = 0; B = X; break;
  17091. default: R = 0; G = 0; B = 0; break;
  17092. }
  17093. return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
  17094. };
  17095. /**
  17096. * Draw all datapoints as a grid
  17097. * This function can be used when the style is 'grid'
  17098. */
  17099. Graph3d.prototype._redrawDataGrid = function() {
  17100. var canvas = this.frame.canvas,
  17101. ctx = canvas.getContext('2d'),
  17102. point, right, top, cross,
  17103. i,
  17104. topSideVisible, fillStyle, strokeStyle, lineWidth,
  17105. h, s, v, zAvg;
  17106. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17107. return; // TODO: throw exception?
  17108. // calculate the translations and screen position of all points
  17109. for (i = 0; i < this.dataPoints.length; i++) {
  17110. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17111. var screen = this._convertTranslationToScreen(trans);
  17112. this.dataPoints[i].trans = trans;
  17113. this.dataPoints[i].screen = screen;
  17114. // calculate the translation of the point at the bottom (needed for sorting)
  17115. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  17116. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  17117. }
  17118. // sort the points on depth of their (x,y) position (not on z)
  17119. var sortDepth = function (a, b) {
  17120. return b.dist - a.dist;
  17121. };
  17122. this.dataPoints.sort(sortDepth);
  17123. if (this.style === Graph3d.STYLE.SURFACE) {
  17124. for (i = 0; i < this.dataPoints.length; i++) {
  17125. point = this.dataPoints[i];
  17126. right = this.dataPoints[i].pointRight;
  17127. top = this.dataPoints[i].pointTop;
  17128. cross = this.dataPoints[i].pointCross;
  17129. if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
  17130. if (this.showGrayBottom || this.showShadow) {
  17131. // calculate the cross product of the two vectors from center
  17132. // to left and right, in order to know whether we are looking at the
  17133. // bottom or at the top side. We can also use the cross product
  17134. // for calculating light intensity
  17135. var aDiff = Point3d.subtract(cross.trans, point.trans);
  17136. var bDiff = Point3d.subtract(top.trans, right.trans);
  17137. var crossproduct = Point3d.crossProduct(aDiff, bDiff);
  17138. var len = crossproduct.length();
  17139. // FIXME: there is a bug with determining the surface side (shadow or colored)
  17140. topSideVisible = (crossproduct.z > 0);
  17141. }
  17142. else {
  17143. topSideVisible = true;
  17144. }
  17145. if (topSideVisible) {
  17146. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17147. zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
  17148. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17149. s = 1; // saturation
  17150. if (this.showShadow) {
  17151. v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
  17152. fillStyle = this._hsv2rgb(h, s, v);
  17153. strokeStyle = fillStyle;
  17154. }
  17155. else {
  17156. v = 1;
  17157. fillStyle = this._hsv2rgb(h, s, v);
  17158. strokeStyle = this.colorAxis;
  17159. }
  17160. }
  17161. else {
  17162. fillStyle = 'gray';
  17163. strokeStyle = this.colorAxis;
  17164. }
  17165. lineWidth = 0.5;
  17166. ctx.lineWidth = lineWidth;
  17167. ctx.fillStyle = fillStyle;
  17168. ctx.strokeStyle = strokeStyle;
  17169. ctx.beginPath();
  17170. ctx.moveTo(point.screen.x, point.screen.y);
  17171. ctx.lineTo(right.screen.x, right.screen.y);
  17172. ctx.lineTo(cross.screen.x, cross.screen.y);
  17173. ctx.lineTo(top.screen.x, top.screen.y);
  17174. ctx.closePath();
  17175. ctx.fill();
  17176. ctx.stroke();
  17177. }
  17178. }
  17179. }
  17180. else { // grid style
  17181. for (i = 0; i < this.dataPoints.length; i++) {
  17182. point = this.dataPoints[i];
  17183. right = this.dataPoints[i].pointRight;
  17184. top = this.dataPoints[i].pointTop;
  17185. if (point !== undefined) {
  17186. if (this.showPerspective) {
  17187. lineWidth = 2 / -point.trans.z;
  17188. }
  17189. else {
  17190. lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
  17191. }
  17192. }
  17193. if (point !== undefined && right !== undefined) {
  17194. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17195. zAvg = (point.point.z + right.point.z) / 2;
  17196. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17197. ctx.lineWidth = lineWidth;
  17198. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  17199. ctx.beginPath();
  17200. ctx.moveTo(point.screen.x, point.screen.y);
  17201. ctx.lineTo(right.screen.x, right.screen.y);
  17202. ctx.stroke();
  17203. }
  17204. if (point !== undefined && top !== undefined) {
  17205. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17206. zAvg = (point.point.z + top.point.z) / 2;
  17207. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17208. ctx.lineWidth = lineWidth;
  17209. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  17210. ctx.beginPath();
  17211. ctx.moveTo(point.screen.x, point.screen.y);
  17212. ctx.lineTo(top.screen.x, top.screen.y);
  17213. ctx.stroke();
  17214. }
  17215. }
  17216. }
  17217. };
  17218. /**
  17219. * Draw all datapoints as dots.
  17220. * This function can be used when the style is 'dot' or 'dot-line'
  17221. */
  17222. Graph3d.prototype._redrawDataDot = function() {
  17223. var canvas = this.frame.canvas;
  17224. var ctx = canvas.getContext('2d');
  17225. var i;
  17226. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17227. return; // TODO: throw exception?
  17228. // calculate the translations of all points
  17229. for (i = 0; i < this.dataPoints.length; i++) {
  17230. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17231. var screen = this._convertTranslationToScreen(trans);
  17232. this.dataPoints[i].trans = trans;
  17233. this.dataPoints[i].screen = screen;
  17234. // calculate the distance from the point at the bottom to the camera
  17235. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  17236. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  17237. }
  17238. // order the translated points by depth
  17239. var sortDepth = function (a, b) {
  17240. return b.dist - a.dist;
  17241. };
  17242. this.dataPoints.sort(sortDepth);
  17243. // draw the datapoints as colored circles
  17244. var dotSize = this.frame.clientWidth * 0.02; // px
  17245. for (i = 0; i < this.dataPoints.length; i++) {
  17246. var point = this.dataPoints[i];
  17247. if (this.style === Graph3d.STYLE.DOTLINE) {
  17248. // draw a vertical line from the bottom to the graph value
  17249. //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
  17250. var from = this._convert3Dto2D(point.bottom);
  17251. ctx.lineWidth = 1;
  17252. ctx.strokeStyle = this.colorGrid;
  17253. ctx.beginPath();
  17254. ctx.moveTo(from.x, from.y);
  17255. ctx.lineTo(point.screen.x, point.screen.y);
  17256. ctx.stroke();
  17257. }
  17258. // calculate radius for the circle
  17259. var size;
  17260. if (this.style === Graph3d.STYLE.DOTSIZE) {
  17261. size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
  17262. }
  17263. else {
  17264. size = dotSize;
  17265. }
  17266. var radius;
  17267. if (this.showPerspective) {
  17268. radius = size / -point.trans.z;
  17269. }
  17270. else {
  17271. radius = size * -(this.eye.z / this.camera.getArmLength());
  17272. }
  17273. if (radius < 0) {
  17274. radius = 0;
  17275. }
  17276. var hue, color, borderColor;
  17277. if (this.style === Graph3d.STYLE.DOTCOLOR ) {
  17278. // calculate the color based on the value
  17279. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  17280. color = this._hsv2rgb(hue, 1, 1);
  17281. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17282. }
  17283. else if (this.style === Graph3d.STYLE.DOTSIZE) {
  17284. color = this.colorDot;
  17285. borderColor = this.colorDotBorder;
  17286. }
  17287. else {
  17288. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17289. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17290. color = this._hsv2rgb(hue, 1, 1);
  17291. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17292. }
  17293. // draw the circle
  17294. ctx.lineWidth = 1.0;
  17295. ctx.strokeStyle = borderColor;
  17296. ctx.fillStyle = color;
  17297. ctx.beginPath();
  17298. ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
  17299. ctx.fill();
  17300. ctx.stroke();
  17301. }
  17302. };
  17303. /**
  17304. * Draw all datapoints as bars.
  17305. * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
  17306. */
  17307. Graph3d.prototype._redrawDataBar = function() {
  17308. var canvas = this.frame.canvas;
  17309. var ctx = canvas.getContext('2d');
  17310. var i, j, surface, corners;
  17311. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17312. return; // TODO: throw exception?
  17313. // calculate the translations of all points
  17314. for (i = 0; i < this.dataPoints.length; i++) {
  17315. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17316. var screen = this._convertTranslationToScreen(trans);
  17317. this.dataPoints[i].trans = trans;
  17318. this.dataPoints[i].screen = screen;
  17319. // calculate the distance from the point at the bottom to the camera
  17320. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  17321. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  17322. }
  17323. // order the translated points by depth
  17324. var sortDepth = function (a, b) {
  17325. return b.dist - a.dist;
  17326. };
  17327. this.dataPoints.sort(sortDepth);
  17328. // draw the datapoints as bars
  17329. var xWidth = this.xBarWidth / 2;
  17330. var yWidth = this.yBarWidth / 2;
  17331. for (i = 0; i < this.dataPoints.length; i++) {
  17332. var point = this.dataPoints[i];
  17333. // determine color
  17334. var hue, color, borderColor;
  17335. if (this.style === Graph3d.STYLE.BARCOLOR ) {
  17336. // calculate the color based on the value
  17337. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  17338. color = this._hsv2rgb(hue, 1, 1);
  17339. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17340. }
  17341. else if (this.style === Graph3d.STYLE.BARSIZE) {
  17342. color = this.colorDot;
  17343. borderColor = this.colorDotBorder;
  17344. }
  17345. else {
  17346. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17347. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17348. color = this._hsv2rgb(hue, 1, 1);
  17349. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17350. }
  17351. // calculate size for the bar
  17352. if (this.style === Graph3d.STYLE.BARSIZE) {
  17353. xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  17354. yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  17355. }
  17356. // calculate all corner points
  17357. var me = this;
  17358. var point3d = point.point;
  17359. var top = [
  17360. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
  17361. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
  17362. {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, point3d.z)},
  17363. {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, point3d.z)}
  17364. ];
  17365. var bottom = [
  17366. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
  17367. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
  17368. {point: new Point3d(point3d.x + xWidth, point3d.y + yWidth, this.zMin)},
  17369. {point: new Point3d(point3d.x - xWidth, point3d.y + yWidth, this.zMin)}
  17370. ];
  17371. // calculate screen location of the points
  17372. top.forEach(function (obj) {
  17373. obj.screen = me._convert3Dto2D(obj.point);
  17374. });
  17375. bottom.forEach(function (obj) {
  17376. obj.screen = me._convert3Dto2D(obj.point);
  17377. });
  17378. // create five sides, calculate both corner points and center points
  17379. var surfaces = [
  17380. {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
  17381. {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
  17382. {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
  17383. {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
  17384. {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
  17385. ];
  17386. point.surfaces = surfaces;
  17387. // calculate the distance of each of the surface centers to the camera
  17388. for (j = 0; j < surfaces.length; j++) {
  17389. surface = surfaces[j];
  17390. var transCenter = this._convertPointToTranslation(surface.center);
  17391. surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
  17392. // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
  17393. // but the current solution is fast/simple and works in 99.9% of all cases
  17394. // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
  17395. }
  17396. // order the surfaces by their (translated) depth
  17397. surfaces.sort(function (a, b) {
  17398. var diff = b.dist - a.dist;
  17399. if (diff) return diff;
  17400. // if equal depth, sort the top surface last
  17401. if (a.corners === top) return 1;
  17402. if (b.corners === top) return -1;
  17403. // both are equal
  17404. return 0;
  17405. });
  17406. // draw the ordered surfaces
  17407. ctx.lineWidth = 1;
  17408. ctx.strokeStyle = borderColor;
  17409. ctx.fillStyle = color;
  17410. // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
  17411. for (j = 2; j < surfaces.length; j++) {
  17412. surface = surfaces[j];
  17413. corners = surface.corners;
  17414. ctx.beginPath();
  17415. ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
  17416. ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
  17417. ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
  17418. ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
  17419. ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
  17420. ctx.fill();
  17421. ctx.stroke();
  17422. }
  17423. }
  17424. };
  17425. /**
  17426. * Draw a line through all datapoints.
  17427. * This function can be used when the style is 'line'
  17428. */
  17429. Graph3d.prototype._redrawDataLine = function() {
  17430. var canvas = this.frame.canvas,
  17431. ctx = canvas.getContext('2d'),
  17432. point, i;
  17433. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17434. return; // TODO: throw exception?
  17435. // calculate the translations of all points
  17436. for (i = 0; i < this.dataPoints.length; i++) {
  17437. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17438. var screen = this._convertTranslationToScreen(trans);
  17439. this.dataPoints[i].trans = trans;
  17440. this.dataPoints[i].screen = screen;
  17441. }
  17442. // start the line
  17443. if (this.dataPoints.length > 0) {
  17444. point = this.dataPoints[0];
  17445. ctx.lineWidth = 1; // TODO: make customizable
  17446. ctx.strokeStyle = 'blue'; // TODO: make customizable
  17447. ctx.beginPath();
  17448. ctx.moveTo(point.screen.x, point.screen.y);
  17449. }
  17450. // draw the datapoints as colored circles
  17451. for (i = 1; i < this.dataPoints.length; i++) {
  17452. point = this.dataPoints[i];
  17453. ctx.lineTo(point.screen.x, point.screen.y);
  17454. }
  17455. // finish the line
  17456. if (this.dataPoints.length > 0) {
  17457. ctx.stroke();
  17458. }
  17459. };
  17460. /**
  17461. * Start a moving operation inside the provided parent element
  17462. * @param {Event} event The event that occurred (required for
  17463. * retrieving the mouse position)
  17464. */
  17465. Graph3d.prototype._onMouseDown = function(event) {
  17466. event = event || window.event;
  17467. // check if mouse is still down (may be up when focus is lost for example
  17468. // in an iframe)
  17469. if (this.leftButtonDown) {
  17470. this._onMouseUp(event);
  17471. }
  17472. // only react on left mouse button down
  17473. this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  17474. if (!this.leftButtonDown && !this.touchDown) return;
  17475. // get mouse position (different code for IE and all other browsers)
  17476. this.startMouseX = getMouseX(event);
  17477. this.startMouseY = getMouseY(event);
  17478. this.startStart = new Date(this.start);
  17479. this.startEnd = new Date(this.end);
  17480. this.startArmRotation = this.camera.getArmRotation();
  17481. this.frame.style.cursor = 'move';
  17482. // add event listeners to handle moving the contents
  17483. // we store the function onmousemove and onmouseup in the graph, so we can
  17484. // remove the eventlisteners lateron in the function mouseUp()
  17485. var me = this;
  17486. this.onmousemove = function (event) {me._onMouseMove(event);};
  17487. this.onmouseup = function (event) {me._onMouseUp(event);};
  17488. G3DaddEventListener(document, 'mousemove', me.onmousemove);
  17489. G3DaddEventListener(document, 'mouseup', me.onmouseup);
  17490. G3DpreventDefault(event);
  17491. };
  17492. /**
  17493. * Perform moving operating.
  17494. * This function activated from within the funcion Graph.mouseDown().
  17495. * @param {Event} event Well, eehh, the event
  17496. */
  17497. Graph3d.prototype._onMouseMove = function (event) {
  17498. event = event || window.event;
  17499. // calculate change in mouse position
  17500. var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
  17501. var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
  17502. var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
  17503. var verticalNew = this.startArmRotation.vertical + diffY / 200;
  17504. var snapAngle = 4; // degrees
  17505. var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
  17506. // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
  17507. // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
  17508. if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
  17509. horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
  17510. }
  17511. if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
  17512. horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
  17513. }
  17514. // snap vertically to nice angles
  17515. if (Math.abs(Math.sin(verticalNew)) < snapValue) {
  17516. verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
  17517. }
  17518. if (Math.abs(Math.cos(verticalNew)) < snapValue) {
  17519. verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
  17520. }
  17521. this.camera.setArmRotation(horizontalNew, verticalNew);
  17522. this.redraw();
  17523. // fire a cameraPositionChange event
  17524. var parameters = this.getCameraPosition();
  17525. this.emit('cameraPositionChange', parameters);
  17526. G3DpreventDefault(event);
  17527. };
  17528. /**
  17529. * Stop moving operating.
  17530. * This function activated from within the funcion Graph.mouseDown().
  17531. * @param {event} event The event
  17532. */
  17533. Graph3d.prototype._onMouseUp = function (event) {
  17534. this.frame.style.cursor = 'auto';
  17535. this.leftButtonDown = false;
  17536. // remove event listeners here
  17537. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  17538. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  17539. G3DpreventDefault(event);
  17540. };
  17541. /**
  17542. * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
  17543. * @param {Event} event A mouse move event
  17544. */
  17545. Graph3d.prototype._onTooltip = function (event) {
  17546. var delay = 300; // ms
  17547. var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame);
  17548. var mouseY = getMouseY(event) - getAbsoluteTop(this.frame);
  17549. if (!this.showTooltip) {
  17550. return;
  17551. }
  17552. if (this.tooltipTimeout) {
  17553. clearTimeout(this.tooltipTimeout);
  17554. }
  17555. // (delayed) display of a tooltip only if no mouse button is down
  17556. if (this.leftButtonDown) {
  17557. this._hideTooltip();
  17558. return;
  17559. }
  17560. if (this.tooltip && this.tooltip.dataPoint) {
  17561. // tooltip is currently visible
  17562. var dataPoint = this._dataPointFromXY(mouseX, mouseY);
  17563. if (dataPoint !== this.tooltip.dataPoint) {
  17564. // datapoint changed
  17565. if (dataPoint) {
  17566. this._showTooltip(dataPoint);
  17567. }
  17568. else {
  17569. this._hideTooltip();
  17570. }
  17571. }
  17572. }
  17573. else {
  17574. // tooltip is currently not visible
  17575. var me = this;
  17576. this.tooltipTimeout = setTimeout(function () {
  17577. me.tooltipTimeout = null;
  17578. // show a tooltip if we have a data point
  17579. var dataPoint = me._dataPointFromXY(mouseX, mouseY);
  17580. if (dataPoint) {
  17581. me._showTooltip(dataPoint);
  17582. }
  17583. }, delay);
  17584. }
  17585. };
  17586. /**
  17587. * Event handler for touchstart event on mobile devices
  17588. */
  17589. Graph3d.prototype._onTouchStart = function(event) {
  17590. this.touchDown = true;
  17591. var me = this;
  17592. this.ontouchmove = function (event) {me._onTouchMove(event);};
  17593. this.ontouchend = function (event) {me._onTouchEnd(event);};
  17594. G3DaddEventListener(document, 'touchmove', me.ontouchmove);
  17595. G3DaddEventListener(document, 'touchend', me.ontouchend);
  17596. this._onMouseDown(event);
  17597. };
  17598. /**
  17599. * Event handler for touchmove event on mobile devices
  17600. */
  17601. Graph3d.prototype._onTouchMove = function(event) {
  17602. this._onMouseMove(event);
  17603. };
  17604. /**
  17605. * Event handler for touchend event on mobile devices
  17606. */
  17607. Graph3d.prototype._onTouchEnd = function(event) {
  17608. this.touchDown = false;
  17609. G3DremoveEventListener(document, 'touchmove', this.ontouchmove);
  17610. G3DremoveEventListener(document, 'touchend', this.ontouchend);
  17611. this._onMouseUp(event);
  17612. };
  17613. /**
  17614. * Event handler for mouse wheel event, used to zoom the graph
  17615. * Code from http://adomas.org/javascript-mouse-wheel/
  17616. * @param {event} event The event
  17617. */
  17618. Graph3d.prototype._onWheel = function(event) {
  17619. if (!event) /* For IE. */
  17620. event = window.event;
  17621. // retrieve delta
  17622. var delta = 0;
  17623. if (event.wheelDelta) { /* IE/Opera. */
  17624. delta = event.wheelDelta/120;
  17625. } else if (event.detail) { /* Mozilla case. */
  17626. // In Mozilla, sign of delta is different than in IE.
  17627. // Also, delta is multiple of 3.
  17628. delta = -event.detail/3;
  17629. }
  17630. // If delta is nonzero, handle it.
  17631. // Basically, delta is now positive if wheel was scrolled up,
  17632. // and negative, if wheel was scrolled down.
  17633. if (delta) {
  17634. var oldLength = this.camera.getArmLength();
  17635. var newLength = oldLength * (1 - delta / 10);
  17636. this.camera.setArmLength(newLength);
  17637. this.redraw();
  17638. this._hideTooltip();
  17639. }
  17640. // fire a cameraPositionChange event
  17641. var parameters = this.getCameraPosition();
  17642. this.emit('cameraPositionChange', parameters);
  17643. // Prevent default actions caused by mouse wheel.
  17644. // That might be ugly, but we handle scrolls somehow
  17645. // anyway, so don't bother here..
  17646. G3DpreventDefault(event);
  17647. };
  17648. /**
  17649. * Test whether a point lies inside given 2D triangle
  17650. * @param {Point2d} point
  17651. * @param {Point2d[]} triangle
  17652. * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
  17653. * @private
  17654. */
  17655. Graph3d.prototype._insideTriangle = function (point, triangle) {
  17656. var a = triangle[0],
  17657. b = triangle[1],
  17658. c = triangle[2];
  17659. function sign (x) {
  17660. return x > 0 ? 1 : x < 0 ? -1 : 0;
  17661. }
  17662. var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
  17663. var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
  17664. var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
  17665. // each of the three signs must be either equal to each other or zero
  17666. return (as == 0 || bs == 0 || as == bs) &&
  17667. (bs == 0 || cs == 0 || bs == cs) &&
  17668. (as == 0 || cs == 0 || as == cs);
  17669. };
  17670. /**
  17671. * Find a data point close to given screen position (x, y)
  17672. * @param {Number} x
  17673. * @param {Number} y
  17674. * @return {Object | null} The closest data point or null if not close to any data point
  17675. * @private
  17676. */
  17677. Graph3d.prototype._dataPointFromXY = function (x, y) {
  17678. var i,
  17679. distMax = 100, // px
  17680. dataPoint = null,
  17681. closestDataPoint = null,
  17682. closestDist = null,
  17683. center = new Point2d(x, y);
  17684. if (this.style === Graph3d.STYLE.BAR ||
  17685. this.style === Graph3d.STYLE.BARCOLOR ||
  17686. this.style === Graph3d.STYLE.BARSIZE) {
  17687. // the data points are ordered from far away to closest
  17688. for (i = this.dataPoints.length - 1; i >= 0; i--) {
  17689. dataPoint = this.dataPoints[i];
  17690. var surfaces = dataPoint.surfaces;
  17691. if (surfaces) {
  17692. for (var s = surfaces.length - 1; s >= 0; s--) {
  17693. // split each surface in two triangles, and see if the center point is inside one of these
  17694. var surface = surfaces[s];
  17695. var corners = surface.corners;
  17696. var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
  17697. var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
  17698. if (this._insideTriangle(center, triangle1) ||
  17699. this._insideTriangle(center, triangle2)) {
  17700. // return immediately at the first hit
  17701. return dataPoint;
  17702. }
  17703. }
  17704. }
  17705. }
  17706. }
  17707. else {
  17708. // find the closest data point, using distance to the center of the point on 2d screen
  17709. for (i = 0; i < this.dataPoints.length; i++) {
  17710. dataPoint = this.dataPoints[i];
  17711. var point = dataPoint.screen;
  17712. if (point) {
  17713. var distX = Math.abs(x - point.x);
  17714. var distY = Math.abs(y - point.y);
  17715. var dist = Math.sqrt(distX * distX + distY * distY);
  17716. if ((closestDist === null || dist < closestDist) && dist < distMax) {
  17717. closestDist = dist;
  17718. closestDataPoint = dataPoint;
  17719. }
  17720. }
  17721. }
  17722. }
  17723. return closestDataPoint;
  17724. };
  17725. /**
  17726. * Display a tooltip for given data point
  17727. * @param {Object} dataPoint
  17728. * @private
  17729. */
  17730. Graph3d.prototype._showTooltip = function (dataPoint) {
  17731. var content, line, dot;
  17732. if (!this.tooltip) {
  17733. content = document.createElement('div');
  17734. content.style.position = 'absolute';
  17735. content.style.padding = '10px';
  17736. content.style.border = '1px solid #4d4d4d';
  17737. content.style.color = '#1a1a1a';
  17738. content.style.background = 'rgba(255,255,255,0.7)';
  17739. content.style.borderRadius = '2px';
  17740. content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
  17741. line = document.createElement('div');
  17742. line.style.position = 'absolute';
  17743. line.style.height = '40px';
  17744. line.style.width = '0';
  17745. line.style.borderLeft = '1px solid #4d4d4d';
  17746. dot = document.createElement('div');
  17747. dot.style.position = 'absolute';
  17748. dot.style.height = '0';
  17749. dot.style.width = '0';
  17750. dot.style.border = '5px solid #4d4d4d';
  17751. dot.style.borderRadius = '5px';
  17752. this.tooltip = {
  17753. dataPoint: null,
  17754. dom: {
  17755. content: content,
  17756. line: line,
  17757. dot: dot
  17758. }
  17759. };
  17760. }
  17761. else {
  17762. content = this.tooltip.dom.content;
  17763. line = this.tooltip.dom.line;
  17764. dot = this.tooltip.dom.dot;
  17765. }
  17766. this._hideTooltip();
  17767. this.tooltip.dataPoint = dataPoint;
  17768. if (typeof this.showTooltip === 'function') {
  17769. content.innerHTML = this.showTooltip(dataPoint.point);
  17770. }
  17771. else {
  17772. content.innerHTML = '<table>' +
  17773. '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' +
  17774. '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' +
  17775. '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' +
  17776. '</table>';
  17777. }
  17778. content.style.left = '0';
  17779. content.style.top = '0';
  17780. this.frame.appendChild(content);
  17781. this.frame.appendChild(line);
  17782. this.frame.appendChild(dot);
  17783. // calculate sizes
  17784. var contentWidth = content.offsetWidth;
  17785. var contentHeight = content.offsetHeight;
  17786. var lineHeight = line.offsetHeight;
  17787. var dotWidth = dot.offsetWidth;
  17788. var dotHeight = dot.offsetHeight;
  17789. var left = dataPoint.screen.x - contentWidth / 2;
  17790. left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
  17791. line.style.left = dataPoint.screen.x + 'px';
  17792. line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
  17793. content.style.left = left + 'px';
  17794. content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
  17795. dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
  17796. dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
  17797. };
  17798. /**
  17799. * Hide the tooltip when displayed
  17800. * @private
  17801. */
  17802. Graph3d.prototype._hideTooltip = function () {
  17803. if (this.tooltip) {
  17804. this.tooltip.dataPoint = null;
  17805. for (var prop in this.tooltip.dom) {
  17806. if (this.tooltip.dom.hasOwnProperty(prop)) {
  17807. var elem = this.tooltip.dom[prop];
  17808. if (elem && elem.parentNode) {
  17809. elem.parentNode.removeChild(elem);
  17810. }
  17811. }
  17812. }
  17813. }
  17814. };
  17815. /**
  17816. * Add and event listener. Works for all browsers
  17817. * @param {Element} element An html element
  17818. * @param {string} action The action, for example 'click',
  17819. * without the prefix 'on'
  17820. * @param {function} listener The callback function to be executed
  17821. * @param {boolean} useCapture
  17822. */
  17823. G3DaddEventListener = function(element, action, listener, useCapture) {
  17824. if (element.addEventListener) {
  17825. if (useCapture === undefined)
  17826. useCapture = false;
  17827. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  17828. action = 'DOMMouseScroll'; // For Firefox
  17829. }
  17830. element.addEventListener(action, listener, useCapture);
  17831. } else {
  17832. element.attachEvent('on' + action, listener); // IE browsers
  17833. }
  17834. };
  17835. /**
  17836. * Remove an event listener from an element
  17837. * @param {Element} element An html dom element
  17838. * @param {string} action The name of the event, for example 'mousedown'
  17839. * @param {function} listener The listener function
  17840. * @param {boolean} useCapture
  17841. */
  17842. G3DremoveEventListener = function(element, action, listener, useCapture) {
  17843. if (element.removeEventListener) {
  17844. // non-IE browsers
  17845. if (useCapture === undefined)
  17846. useCapture = false;
  17847. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  17848. action = 'DOMMouseScroll'; // For Firefox
  17849. }
  17850. element.removeEventListener(action, listener, useCapture);
  17851. } else {
  17852. // IE browsers
  17853. element.detachEvent('on' + action, listener);
  17854. }
  17855. };
  17856. /**
  17857. * Stop event propagation
  17858. */
  17859. G3DstopPropagation = function(event) {
  17860. if (!event)
  17861. event = window.event;
  17862. if (event.stopPropagation) {
  17863. event.stopPropagation(); // non-IE browsers
  17864. }
  17865. else {
  17866. event.cancelBubble = true; // IE browsers
  17867. }
  17868. };
  17869. /**
  17870. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  17871. */
  17872. G3DpreventDefault = function (event) {
  17873. if (!event)
  17874. event = window.event;
  17875. if (event.preventDefault) {
  17876. event.preventDefault(); // non-IE browsers
  17877. }
  17878. else {
  17879. event.returnValue = false; // IE browsers
  17880. }
  17881. };
  17882. /**
  17883. * @prototype Point3d
  17884. * @param {Number} x
  17885. * @param {Number} y
  17886. * @param {Number} z
  17887. */
  17888. function Point3d(x, y, z) {
  17889. this.x = x !== undefined ? x : 0;
  17890. this.y = y !== undefined ? y : 0;
  17891. this.z = z !== undefined ? z : 0;
  17892. };
  17893. /**
  17894. * Subtract the two provided points, returns a-b
  17895. * @param {Point3d} a
  17896. * @param {Point3d} b
  17897. * @return {Point3d} a-b
  17898. */
  17899. Point3d.subtract = function(a, b) {
  17900. var sub = new Point3d();
  17901. sub.x = a.x - b.x;
  17902. sub.y = a.y - b.y;
  17903. sub.z = a.z - b.z;
  17904. return sub;
  17905. };
  17906. /**
  17907. * Add the two provided points, returns a+b
  17908. * @param {Point3d} a
  17909. * @param {Point3d} b
  17910. * @return {Point3d} a+b
  17911. */
  17912. Point3d.add = function(a, b) {
  17913. var sum = new Point3d();
  17914. sum.x = a.x + b.x;
  17915. sum.y = a.y + b.y;
  17916. sum.z = a.z + b.z;
  17917. return sum;
  17918. };
  17919. /**
  17920. * Calculate the average of two 3d points
  17921. * @param {Point3d} a
  17922. * @param {Point3d} b
  17923. * @return {Point3d} The average, (a+b)/2
  17924. */
  17925. Point3d.avg = function(a, b) {
  17926. return new Point3d(
  17927. (a.x + b.x) / 2,
  17928. (a.y + b.y) / 2,
  17929. (a.z + b.z) / 2
  17930. );
  17931. };
  17932. /**
  17933. * Calculate the cross product of the two provided points, returns axb
  17934. * Documentation: http://en.wikipedia.org/wiki/Cross_product
  17935. * @param {Point3d} a
  17936. * @param {Point3d} b
  17937. * @return {Point3d} cross product axb
  17938. */
  17939. Point3d.crossProduct = function(a, b) {
  17940. var crossproduct = new Point3d();
  17941. crossproduct.x = a.y * b.z - a.z * b.y;
  17942. crossproduct.y = a.z * b.x - a.x * b.z;
  17943. crossproduct.z = a.x * b.y - a.y * b.x;
  17944. return crossproduct;
  17945. };
  17946. /**
  17947. * Rtrieve the length of the vector (or the distance from this point to the origin
  17948. * @return {Number} length
  17949. */
  17950. Point3d.prototype.length = function() {
  17951. return Math.sqrt(
  17952. this.x * this.x +
  17953. this.y * this.y +
  17954. this.z * this.z
  17955. );
  17956. };
  17957. /**
  17958. * @prototype Point2d
  17959. */
  17960. Point2d = function (x, y) {
  17961. this.x = x !== undefined ? x : 0;
  17962. this.y = y !== undefined ? y : 0;
  17963. };
  17964. /**
  17965. * @class Filter
  17966. *
  17967. * @param {DataSet} data The google data table
  17968. * @param {Number} column The index of the column to be filtered
  17969. * @param {Graph} graph The graph
  17970. */
  17971. function Filter (data, column, graph) {
  17972. this.data = data;
  17973. this.column = column;
  17974. this.graph = graph; // the parent graph
  17975. this.index = undefined;
  17976. this.value = undefined;
  17977. // read all distinct values and select the first one
  17978. this.values = graph.getDistinctValues(data.get(), this.column);
  17979. // sort both numeric and string values correctly
  17980. this.values.sort(function (a, b) {
  17981. return a > b ? 1 : a < b ? -1 : 0;
  17982. });
  17983. if (this.values.length > 0) {
  17984. this.selectValue(0);
  17985. }
  17986. // create an array with the filtered datapoints. this will be loaded afterwards
  17987. this.dataPoints = [];
  17988. this.loaded = false;
  17989. this.onLoadCallback = undefined;
  17990. if (graph.animationPreload) {
  17991. this.loaded = false;
  17992. this.loadInBackground();
  17993. }
  17994. else {
  17995. this.loaded = true;
  17996. }
  17997. };
  17998. /**
  17999. * Return the label
  18000. * @return {string} label
  18001. */
  18002. Filter.prototype.isLoaded = function() {
  18003. return this.loaded;
  18004. };
  18005. /**
  18006. * Return the loaded progress
  18007. * @return {Number} percentage between 0 and 100
  18008. */
  18009. Filter.prototype.getLoadedProgress = function() {
  18010. var len = this.values.length;
  18011. var i = 0;
  18012. while (this.dataPoints[i]) {
  18013. i++;
  18014. }
  18015. return Math.round(i / len * 100);
  18016. };
  18017. /**
  18018. * Return the label
  18019. * @return {string} label
  18020. */
  18021. Filter.prototype.getLabel = function() {
  18022. return this.graph.filterLabel;
  18023. };
  18024. /**
  18025. * Return the columnIndex of the filter
  18026. * @return {Number} columnIndex
  18027. */
  18028. Filter.prototype.getColumn = function() {
  18029. return this.column;
  18030. };
  18031. /**
  18032. * Return the currently selected value. Returns undefined if there is no selection
  18033. * @return {*} value
  18034. */
  18035. Filter.prototype.getSelectedValue = function() {
  18036. if (this.index === undefined)
  18037. return undefined;
  18038. return this.values[this.index];
  18039. };
  18040. /**
  18041. * Retrieve all values of the filter
  18042. * @return {Array} values
  18043. */
  18044. Filter.prototype.getValues = function() {
  18045. return this.values;
  18046. };
  18047. /**
  18048. * Retrieve one value of the filter
  18049. * @param {Number} index
  18050. * @return {*} value
  18051. */
  18052. Filter.prototype.getValue = function(index) {
  18053. if (index >= this.values.length)
  18054. throw 'Error: index out of range';
  18055. return this.values[index];
  18056. };
  18057. /**
  18058. * Retrieve the (filtered) dataPoints for the currently selected filter index
  18059. * @param {Number} [index] (optional)
  18060. * @return {Array} dataPoints
  18061. */
  18062. Filter.prototype._getDataPoints = function(index) {
  18063. if (index === undefined)
  18064. index = this.index;
  18065. if (index === undefined)
  18066. return [];
  18067. var dataPoints;
  18068. if (this.dataPoints[index]) {
  18069. dataPoints = this.dataPoints[index];
  18070. }
  18071. else {
  18072. var f = {};
  18073. f.column = this.column;
  18074. f.value = this.values[index];
  18075. var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
  18076. dataPoints = this.graph._getDataPoints(dataView);
  18077. this.dataPoints[index] = dataPoints;
  18078. }
  18079. return dataPoints;
  18080. };
  18081. /**
  18082. * Set a callback function when the filter is fully loaded.
  18083. */
  18084. Filter.prototype.setOnLoadCallback = function(callback) {
  18085. this.onLoadCallback = callback;
  18086. };
  18087. /**
  18088. * Add a value to the list with available values for this filter
  18089. * No double entries will be created.
  18090. * @param {Number} index
  18091. */
  18092. Filter.prototype.selectValue = function(index) {
  18093. if (index >= this.values.length)
  18094. throw 'Error: index out of range';
  18095. this.index = index;
  18096. this.value = this.values[index];
  18097. };
  18098. /**
  18099. * Load all filtered rows in the background one by one
  18100. * Start this method without providing an index!
  18101. */
  18102. Filter.prototype.loadInBackground = function(index) {
  18103. if (index === undefined)
  18104. index = 0;
  18105. var frame = this.graph.frame;
  18106. if (index < this.values.length) {
  18107. var dataPointsTemp = this._getDataPoints(index);
  18108. //this.graph.redrawInfo(); // TODO: not neat
  18109. // create a progress box
  18110. if (frame.progress === undefined) {
  18111. frame.progress = document.createElement('DIV');
  18112. frame.progress.style.position = 'absolute';
  18113. frame.progress.style.color = 'gray';
  18114. frame.appendChild(frame.progress);
  18115. }
  18116. var progress = this.getLoadedProgress();
  18117. frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
  18118. // TODO: this is no nice solution...
  18119. frame.progress.style.bottom = Graph3d.px(60); // TODO: use height of slider
  18120. frame.progress.style.left = Graph3d.px(10);
  18121. var me = this;
  18122. setTimeout(function() {me.loadInBackground(index+1);}, 10);
  18123. this.loaded = false;
  18124. }
  18125. else {
  18126. this.loaded = true;
  18127. // remove the progress box
  18128. if (frame.progress !== undefined) {
  18129. frame.removeChild(frame.progress);
  18130. frame.progress = undefined;
  18131. }
  18132. if (this.onLoadCallback)
  18133. this.onLoadCallback();
  18134. }
  18135. };
  18136. /**
  18137. * @prototype StepNumber
  18138. * The class StepNumber is an iterator for Numbers. You provide a start and end
  18139. * value, and a best step size. StepNumber itself rounds to fixed values and
  18140. * a finds the step that best fits the provided step.
  18141. *
  18142. * If prettyStep is true, the step size is chosen as close as possible to the
  18143. * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
  18144. *
  18145. * Example usage:
  18146. * var step = new StepNumber(0, 10, 2.5, true);
  18147. * step.start();
  18148. * while (!step.end()) {
  18149. * alert(step.getCurrent());
  18150. * step.next();
  18151. * }
  18152. *
  18153. * Version: 1.0
  18154. *
  18155. * @param {Number} start The start value
  18156. * @param {Number} end The end value
  18157. * @param {Number} step Optional. Step size. Must be a positive value.
  18158. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  18159. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  18160. */
  18161. StepNumber = function (start, end, step, prettyStep) {
  18162. // set default values
  18163. this._start = 0;
  18164. this._end = 0;
  18165. this._step = 1;
  18166. this.prettyStep = true;
  18167. this.precision = 5;
  18168. this._current = 0;
  18169. this.setRange(start, end, step, prettyStep);
  18170. };
  18171. /**
  18172. * Set a new range: start, end and step.
  18173. *
  18174. * @param {Number} start The start value
  18175. * @param {Number} end The end value
  18176. * @param {Number} step Optional. Step size. Must be a positive value.
  18177. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  18178. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  18179. */
  18180. StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
  18181. this._start = start ? start : 0;
  18182. this._end = end ? end : 0;
  18183. this.setStep(step, prettyStep);
  18184. };
  18185. /**
  18186. * Set a new step size
  18187. * @param {Number} step New step size. Must be a positive value
  18188. * @param {boolean} prettyStep Optional. If true, the provided step is rounded
  18189. * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  18190. */
  18191. StepNumber.prototype.setStep = function(step, prettyStep) {
  18192. if (step === undefined || step <= 0)
  18193. return;
  18194. if (prettyStep !== undefined)
  18195. this.prettyStep = prettyStep;
  18196. if (this.prettyStep === true)
  18197. this._step = StepNumber.calculatePrettyStep(step);
  18198. else
  18199. this._step = step;
  18200. };
  18201. /**
  18202. * Calculate a nice step size, closest to the desired step size.
  18203. * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
  18204. * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
  18205. * @param {Number} step Desired step size
  18206. * @return {Number} Nice step size
  18207. */
  18208. StepNumber.calculatePrettyStep = function (step) {
  18209. var log10 = function (x) {return Math.log(x) / Math.LN10;};
  18210. // try three steps (multiple of 1, 2, or 5
  18211. var step1 = Math.pow(10, Math.round(log10(step))),
  18212. step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
  18213. step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
  18214. // choose the best step (closest to minimum step)
  18215. var prettyStep = step1;
  18216. if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
  18217. if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
  18218. // for safety
  18219. if (prettyStep <= 0) {
  18220. prettyStep = 1;
  18221. }
  18222. return prettyStep;
  18223. };
  18224. /**
  18225. * returns the current value of the step
  18226. * @return {Number} current value
  18227. */
  18228. StepNumber.prototype.getCurrent = function () {
  18229. return parseFloat(this._current.toPrecision(this.precision));
  18230. };
  18231. /**
  18232. * returns the current step size
  18233. * @return {Number} current step size
  18234. */
  18235. StepNumber.prototype.getStep = function () {
  18236. return this._step;
  18237. };
  18238. /**
  18239. * Set the current value to the largest value smaller than start, which
  18240. * is a multiple of the step size
  18241. */
  18242. StepNumber.prototype.start = function() {
  18243. this._current = this._start - this._start % this._step;
  18244. };
  18245. /**
  18246. * Do a step, add the step size to the current value
  18247. */
  18248. StepNumber.prototype.next = function () {
  18249. this._current += this._step;
  18250. };
  18251. /**
  18252. * Returns true whether the end is reached
  18253. * @return {boolean} True if the current value has passed the end value.
  18254. */
  18255. StepNumber.prototype.end = function () {
  18256. return (this._current > this._end);
  18257. };
  18258. /**
  18259. * @constructor Slider
  18260. *
  18261. * An html slider control with start/stop/prev/next buttons
  18262. * @param {Element} container The element where the slider will be created
  18263. * @param {Object} options Available options:
  18264. * {boolean} visible If true (default) the
  18265. * slider is visible.
  18266. */
  18267. function Slider(container, options) {
  18268. if (container === undefined) {
  18269. throw 'Error: No container element defined';
  18270. }
  18271. this.container = container;
  18272. this.visible = (options && options.visible != undefined) ? options.visible : true;
  18273. if (this.visible) {
  18274. this.frame = document.createElement('DIV');
  18275. //this.frame.style.backgroundColor = '#E5E5E5';
  18276. this.frame.style.width = '100%';
  18277. this.frame.style.position = 'relative';
  18278. this.container.appendChild(this.frame);
  18279. this.frame.prev = document.createElement('INPUT');
  18280. this.frame.prev.type = 'BUTTON';
  18281. this.frame.prev.value = 'Prev';
  18282. this.frame.appendChild(this.frame.prev);
  18283. this.frame.play = document.createElement('INPUT');
  18284. this.frame.play.type = 'BUTTON';
  18285. this.frame.play.value = 'Play';
  18286. this.frame.appendChild(this.frame.play);
  18287. this.frame.next = document.createElement('INPUT');
  18288. this.frame.next.type = 'BUTTON';
  18289. this.frame.next.value = 'Next';
  18290. this.frame.appendChild(this.frame.next);
  18291. this.frame.bar = document.createElement('INPUT');
  18292. this.frame.bar.type = 'BUTTON';
  18293. this.frame.bar.style.position = 'absolute';
  18294. this.frame.bar.style.border = '1px solid red';
  18295. this.frame.bar.style.width = '100px';
  18296. this.frame.bar.style.height = '6px';
  18297. this.frame.bar.style.borderRadius = '2px';
  18298. this.frame.bar.style.MozBorderRadius = '2px';
  18299. this.frame.bar.style.border = '1px solid #7F7F7F';
  18300. this.frame.bar.style.backgroundColor = '#E5E5E5';
  18301. this.frame.appendChild(this.frame.bar);
  18302. this.frame.slide = document.createElement('INPUT');
  18303. this.frame.slide.type = 'BUTTON';
  18304. this.frame.slide.style.margin = '0px';
  18305. this.frame.slide.value = ' ';
  18306. this.frame.slide.style.position = 'relative';
  18307. this.frame.slide.style.left = '-100px';
  18308. this.frame.appendChild(this.frame.slide);
  18309. // create events
  18310. var me = this;
  18311. this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
  18312. this.frame.prev.onclick = function (event) {me.prev(event);};
  18313. this.frame.play.onclick = function (event) {me.togglePlay(event);};
  18314. this.frame.next.onclick = function (event) {me.next(event);};
  18315. }
  18316. this.onChangeCallback = undefined;
  18317. this.values = [];
  18318. this.index = undefined;
  18319. this.playTimeout = undefined;
  18320. this.playInterval = 1000; // milliseconds
  18321. this.playLoop = true;
  18322. };
  18323. /**
  18324. * Select the previous index
  18325. */
  18326. Slider.prototype.prev = function() {
  18327. var index = this.getIndex();
  18328. if (index > 0) {
  18329. index--;
  18330. this.setIndex(index);
  18331. }
  18332. };
  18333. /**
  18334. * Select the next index
  18335. */
  18336. Slider.prototype.next = function() {
  18337. var index = this.getIndex();
  18338. if (index < this.values.length - 1) {
  18339. index++;
  18340. this.setIndex(index);
  18341. }
  18342. };
  18343. /**
  18344. * Select the next index
  18345. */
  18346. Slider.prototype.playNext = function() {
  18347. var start = new Date();
  18348. var index = this.getIndex();
  18349. if (index < this.values.length - 1) {
  18350. index++;
  18351. this.setIndex(index);
  18352. }
  18353. else if (this.playLoop) {
  18354. // jump to the start
  18355. index = 0;
  18356. this.setIndex(index);
  18357. }
  18358. var end = new Date();
  18359. var diff = (end - start);
  18360. // calculate how much time it to to set the index and to execute the callback
  18361. // function.
  18362. var interval = Math.max(this.playInterval - diff, 0);
  18363. // document.title = diff // TODO: cleanup
  18364. var me = this;
  18365. this.playTimeout = setTimeout(function() {me.playNext();}, interval);
  18366. };
  18367. /**
  18368. * Toggle start or stop playing
  18369. */
  18370. Slider.prototype.togglePlay = function() {
  18371. if (this.playTimeout === undefined) {
  18372. this.play();
  18373. } else {
  18374. this.stop();
  18375. }
  18376. };
  18377. /**
  18378. * Start playing
  18379. */
  18380. Slider.prototype.play = function() {
  18381. // Test whether already playing
  18382. if (this.playTimeout) return;
  18383. this.playNext();
  18384. if (this.frame) {
  18385. this.frame.play.value = 'Stop';
  18386. }
  18387. };
  18388. /**
  18389. * Stop playing
  18390. */
  18391. Slider.prototype.stop = function() {
  18392. clearInterval(this.playTimeout);
  18393. this.playTimeout = undefined;
  18394. if (this.frame) {
  18395. this.frame.play.value = 'Play';
  18396. }
  18397. };
  18398. /**
  18399. * Set a callback function which will be triggered when the value of the
  18400. * slider bar has changed.
  18401. */
  18402. Slider.prototype.setOnChangeCallback = function(callback) {
  18403. this.onChangeCallback = callback;
  18404. };
  18405. /**
  18406. * Set the interval for playing the list
  18407. * @param {Number} interval The interval in milliseconds
  18408. */
  18409. Slider.prototype.setPlayInterval = function(interval) {
  18410. this.playInterval = interval;
  18411. };
  18412. /**
  18413. * Retrieve the current play interval
  18414. * @return {Number} interval The interval in milliseconds
  18415. */
  18416. Slider.prototype.getPlayInterval = function(interval) {
  18417. return this.playInterval;
  18418. };
  18419. /**
  18420. * Set looping on or off
  18421. * @pararm {boolean} doLoop If true, the slider will jump to the start when
  18422. * the end is passed, and will jump to the end
  18423. * when the start is passed.
  18424. */
  18425. Slider.prototype.setPlayLoop = function(doLoop) {
  18426. this.playLoop = doLoop;
  18427. };
  18428. /**
  18429. * Execute the onchange callback function
  18430. */
  18431. Slider.prototype.onChange = function() {
  18432. if (this.onChangeCallback !== undefined) {
  18433. this.onChangeCallback();
  18434. }
  18435. };
  18436. /**
  18437. * redraw the slider on the correct place
  18438. */
  18439. Slider.prototype.redraw = function() {
  18440. if (this.frame) {
  18441. // resize the bar
  18442. this.frame.bar.style.top = (this.frame.clientHeight/2 -
  18443. this.frame.bar.offsetHeight/2) + 'px';
  18444. this.frame.bar.style.width = (this.frame.clientWidth -
  18445. this.frame.prev.clientWidth -
  18446. this.frame.play.clientWidth -
  18447. this.frame.next.clientWidth - 30) + 'px';
  18448. // position the slider button
  18449. var left = this.indexToLeft(this.index);
  18450. this.frame.slide.style.left = (left) + 'px';
  18451. }
  18452. };
  18453. /**
  18454. * Set the list with values for the slider
  18455. * @param {Array} values A javascript array with values (any type)
  18456. */
  18457. Slider.prototype.setValues = function(values) {
  18458. this.values = values;
  18459. if (this.values.length > 0)
  18460. this.setIndex(0);
  18461. else
  18462. this.index = undefined;
  18463. };
  18464. /**
  18465. * Select a value by its index
  18466. * @param {Number} index
  18467. */
  18468. Slider.prototype.setIndex = function(index) {
  18469. if (index < this.values.length) {
  18470. this.index = index;
  18471. this.redraw();
  18472. this.onChange();
  18473. }
  18474. else {
  18475. throw 'Error: index out of range';
  18476. }
  18477. };
  18478. /**
  18479. * retrieve the index of the currently selected vaue
  18480. * @return {Number} index
  18481. */
  18482. Slider.prototype.getIndex = function() {
  18483. return this.index;
  18484. };
  18485. /**
  18486. * retrieve the currently selected value
  18487. * @return {*} value
  18488. */
  18489. Slider.prototype.get = function() {
  18490. return this.values[this.index];
  18491. };
  18492. Slider.prototype._onMouseDown = function(event) {
  18493. // only react on left mouse button down
  18494. var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  18495. if (!leftButtonDown) return;
  18496. this.startClientX = event.clientX;
  18497. this.startSlideX = parseFloat(this.frame.slide.style.left);
  18498. this.frame.style.cursor = 'move';
  18499. // add event listeners to handle moving the contents
  18500. // we store the function onmousemove and onmouseup in the graph, so we can
  18501. // remove the eventlisteners lateron in the function mouseUp()
  18502. var me = this;
  18503. this.onmousemove = function (event) {me._onMouseMove(event);};
  18504. this.onmouseup = function (event) {me._onMouseUp(event);};
  18505. G3DaddEventListener(document, 'mousemove', this.onmousemove);
  18506. G3DaddEventListener(document, 'mouseup', this.onmouseup);
  18507. G3DpreventDefault(event);
  18508. };
  18509. Slider.prototype.leftToIndex = function (left) {
  18510. var width = parseFloat(this.frame.bar.style.width) -
  18511. this.frame.slide.clientWidth - 10;
  18512. var x = left - 3;
  18513. var index = Math.round(x / width * (this.values.length-1));
  18514. if (index < 0) index = 0;
  18515. if (index > this.values.length-1) index = this.values.length-1;
  18516. return index;
  18517. };
  18518. Slider.prototype.indexToLeft = function (index) {
  18519. var width = parseFloat(this.frame.bar.style.width) -
  18520. this.frame.slide.clientWidth - 10;
  18521. var x = index / (this.values.length-1) * width;
  18522. var left = x + 3;
  18523. return left;
  18524. };
  18525. Slider.prototype._onMouseMove = function (event) {
  18526. var diff = event.clientX - this.startClientX;
  18527. var x = this.startSlideX + diff;
  18528. var index = this.leftToIndex(x);
  18529. this.setIndex(index);
  18530. G3DpreventDefault();
  18531. };
  18532. Slider.prototype._onMouseUp = function (event) {
  18533. this.frame.style.cursor = 'auto';
  18534. // remove event listeners
  18535. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  18536. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  18537. G3DpreventDefault();
  18538. };
  18539. /**--------------------------------------------------------------------------**/
  18540. /**
  18541. * Retrieve the absolute left value of a DOM element
  18542. * @param {Element} elem A dom element, for example a div
  18543. * @return {Number} left The absolute left position of this element
  18544. * in the browser page.
  18545. */
  18546. getAbsoluteLeft = function(elem) {
  18547. var left = 0;
  18548. while( elem !== null ) {
  18549. left += elem.offsetLeft;
  18550. left -= elem.scrollLeft;
  18551. elem = elem.offsetParent;
  18552. }
  18553. return left;
  18554. };
  18555. /**
  18556. * Retrieve the absolute top value of a DOM element
  18557. * @param {Element} elem A dom element, for example a div
  18558. * @return {Number} top The absolute top position of this element
  18559. * in the browser page.
  18560. */
  18561. getAbsoluteTop = function(elem) {
  18562. var top = 0;
  18563. while( elem !== null ) {
  18564. top += elem.offsetTop;
  18565. top -= elem.scrollTop;
  18566. elem = elem.offsetParent;
  18567. }
  18568. return top;
  18569. };
  18570. /**
  18571. * Get the horizontal mouse position from a mouse event
  18572. * @param {Event} event
  18573. * @return {Number} mouse x
  18574. */
  18575. getMouseX = function(event) {
  18576. if ('clientX' in event) return event.clientX;
  18577. return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
  18578. };
  18579. /**
  18580. * Get the vertical mouse position from a mouse event
  18581. * @param {Event} event
  18582. * @return {Number} mouse y
  18583. */
  18584. getMouseY = function(event) {
  18585. if ('clientY' in event) return event.clientY;
  18586. return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
  18587. };
  18588. /**
  18589. * vis.js module exports
  18590. */
  18591. var vis = {
  18592. util: util,
  18593. moment: moment,
  18594. DataSet: DataSet,
  18595. DataView: DataView,
  18596. Range: Range,
  18597. stack: stack,
  18598. TimeStep: TimeStep,
  18599. components: {
  18600. items: {
  18601. Item: Item,
  18602. ItemBox: ItemBox,
  18603. ItemPoint: ItemPoint,
  18604. ItemRange: ItemRange
  18605. },
  18606. Component: Component,
  18607. Panel: Panel,
  18608. RootPanel: RootPanel,
  18609. ItemSet: ItemSet,
  18610. TimeAxis: TimeAxis
  18611. },
  18612. graph: {
  18613. Node: Node,
  18614. Edge: Edge,
  18615. Popup: Popup,
  18616. Groups: Groups,
  18617. Images: Images
  18618. },
  18619. Timeline: Timeline,
  18620. Graph: Graph,
  18621. Graph3d: Graph3d
  18622. };
  18623. /**
  18624. * CommonJS module exports
  18625. */
  18626. if (typeof exports !== 'undefined') {
  18627. exports = vis;
  18628. }
  18629. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  18630. module.exports = vis;
  18631. }
  18632. /**
  18633. * AMD module exports
  18634. */
  18635. if (typeof(define) === 'function') {
  18636. define(function () {
  18637. return vis;
  18638. });
  18639. }
  18640. /**
  18641. * Window exports
  18642. */
  18643. if (typeof window !== 'undefined') {
  18644. // attach the module to the window, load as a regular javascript file
  18645. window['vis'] = vis;
  18646. }
  18647. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  18648. /**
  18649. * Expose `Emitter`.
  18650. */
  18651. module.exports = Emitter;
  18652. /**
  18653. * Initialize a new `Emitter`.
  18654. *
  18655. * @api public
  18656. */
  18657. function Emitter(obj) {
  18658. if (obj) return mixin(obj);
  18659. };
  18660. /**
  18661. * Mixin the emitter properties.
  18662. *
  18663. * @param {Object} obj
  18664. * @return {Object}
  18665. * @api private
  18666. */
  18667. function mixin(obj) {
  18668. for (var key in Emitter.prototype) {
  18669. obj[key] = Emitter.prototype[key];
  18670. }
  18671. return obj;
  18672. }
  18673. /**
  18674. * Listen on the given `event` with `fn`.
  18675. *
  18676. * @param {String} event
  18677. * @param {Function} fn
  18678. * @return {Emitter}
  18679. * @api public
  18680. */
  18681. Emitter.prototype.on =
  18682. Emitter.prototype.addEventListener = function(event, fn){
  18683. this._callbacks = this._callbacks || {};
  18684. (this._callbacks[event] = this._callbacks[event] || [])
  18685. .push(fn);
  18686. return this;
  18687. };
  18688. /**
  18689. * Adds an `event` listener that will be invoked a single
  18690. * time then automatically removed.
  18691. *
  18692. * @param {String} event
  18693. * @param {Function} fn
  18694. * @return {Emitter}
  18695. * @api public
  18696. */
  18697. Emitter.prototype.once = function(event, fn){
  18698. var self = this;
  18699. this._callbacks = this._callbacks || {};
  18700. function on() {
  18701. self.off(event, on);
  18702. fn.apply(this, arguments);
  18703. }
  18704. on.fn = fn;
  18705. this.on(event, on);
  18706. return this;
  18707. };
  18708. /**
  18709. * Remove the given callback for `event` or all
  18710. * registered callbacks.
  18711. *
  18712. * @param {String} event
  18713. * @param {Function} fn
  18714. * @return {Emitter}
  18715. * @api public
  18716. */
  18717. Emitter.prototype.off =
  18718. Emitter.prototype.removeListener =
  18719. Emitter.prototype.removeAllListeners =
  18720. Emitter.prototype.removeEventListener = function(event, fn){
  18721. this._callbacks = this._callbacks || {};
  18722. // all
  18723. if (0 == arguments.length) {
  18724. this._callbacks = {};
  18725. return this;
  18726. }
  18727. // specific event
  18728. var callbacks = this._callbacks[event];
  18729. if (!callbacks) return this;
  18730. // remove all handlers
  18731. if (1 == arguments.length) {
  18732. delete this._callbacks[event];
  18733. return this;
  18734. }
  18735. // remove specific handler
  18736. var cb;
  18737. for (var i = 0; i < callbacks.length; i++) {
  18738. cb = callbacks[i];
  18739. if (cb === fn || cb.fn === fn) {
  18740. callbacks.splice(i, 1);
  18741. break;
  18742. }
  18743. }
  18744. return this;
  18745. };
  18746. /**
  18747. * Emit `event` with the given args.
  18748. *
  18749. * @param {String} event
  18750. * @param {Mixed} ...
  18751. * @return {Emitter}
  18752. */
  18753. Emitter.prototype.emit = function(event){
  18754. this._callbacks = this._callbacks || {};
  18755. var args = [].slice.call(arguments, 1)
  18756. , callbacks = this._callbacks[event];
  18757. if (callbacks) {
  18758. callbacks = callbacks.slice(0);
  18759. for (var i = 0, len = callbacks.length; i < len; ++i) {
  18760. callbacks[i].apply(this, args);
  18761. }
  18762. }
  18763. return this;
  18764. };
  18765. /**
  18766. * Return array of callbacks for `event`.
  18767. *
  18768. * @param {String} event
  18769. * @return {Array}
  18770. * @api public
  18771. */
  18772. Emitter.prototype.listeners = function(event){
  18773. this._callbacks = this._callbacks || {};
  18774. return this._callbacks[event] || [];
  18775. };
  18776. /**
  18777. * Check if this emitter has `event` handlers.
  18778. *
  18779. * @param {String} event
  18780. * @return {Boolean}
  18781. * @api public
  18782. */
  18783. Emitter.prototype.hasListeners = function(event){
  18784. return !! this.listeners(event).length;
  18785. };
  18786. },{}],3:[function(require,module,exports){
  18787. /*! Hammer.JS - v1.0.5 - 2013-04-07
  18788. * http://eightmedia.github.com/hammer.js
  18789. *
  18790. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  18791. * Licensed under the MIT license */
  18792. (function(window, undefined) {
  18793. 'use strict';
  18794. /**
  18795. * Hammer
  18796. * use this to create instances
  18797. * @param {HTMLElement} element
  18798. * @param {Object} options
  18799. * @returns {Hammer.Instance}
  18800. * @constructor
  18801. */
  18802. var Hammer = function(element, options) {
  18803. return new Hammer.Instance(element, options || {});
  18804. };
  18805. // default settings
  18806. Hammer.defaults = {
  18807. // add styles and attributes to the element to prevent the browser from doing
  18808. // its native behavior. this doesnt prevent the scrolling, but cancels
  18809. // the contextmenu, tap highlighting etc
  18810. // set to false to disable this
  18811. stop_browser_behavior: {
  18812. // this also triggers onselectstart=false for IE
  18813. userSelect: 'none',
  18814. // this makes the element blocking in IE10 >, you could experiment with the value
  18815. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  18816. touchAction: 'none',
  18817. touchCallout: 'none',
  18818. contentZooming: 'none',
  18819. userDrag: 'none',
  18820. tapHighlightColor: 'rgba(0,0,0,0)'
  18821. }
  18822. // more settings are defined per gesture at gestures.js
  18823. };
  18824. // detect touchevents
  18825. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  18826. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  18827. // dont use mouseevents on mobile devices
  18828. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  18829. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  18830. // eventtypes per touchevent (start, move, end)
  18831. // are filled by Hammer.event.determineEventTypes on setup
  18832. Hammer.EVENT_TYPES = {};
  18833. // direction defines
  18834. Hammer.DIRECTION_DOWN = 'down';
  18835. Hammer.DIRECTION_LEFT = 'left';
  18836. Hammer.DIRECTION_UP = 'up';
  18837. Hammer.DIRECTION_RIGHT = 'right';
  18838. // pointer type
  18839. Hammer.POINTER_MOUSE = 'mouse';
  18840. Hammer.POINTER_TOUCH = 'touch';
  18841. Hammer.POINTER_PEN = 'pen';
  18842. // touch event defines
  18843. Hammer.EVENT_START = 'start';
  18844. Hammer.EVENT_MOVE = 'move';
  18845. Hammer.EVENT_END = 'end';
  18846. // hammer document where the base events are added at
  18847. Hammer.DOCUMENT = document;
  18848. // plugins namespace
  18849. Hammer.plugins = {};
  18850. // if the window events are set...
  18851. Hammer.READY = false;
  18852. /**
  18853. * setup events to detect gestures on the document
  18854. */
  18855. function setup() {
  18856. if(Hammer.READY) {
  18857. return;
  18858. }
  18859. // find what eventtypes we add listeners to
  18860. Hammer.event.determineEventTypes();
  18861. // Register all gestures inside Hammer.gestures
  18862. for(var name in Hammer.gestures) {
  18863. if(Hammer.gestures.hasOwnProperty(name)) {
  18864. Hammer.detection.register(Hammer.gestures[name]);
  18865. }
  18866. }
  18867. // Add touch events on the document
  18868. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  18869. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  18870. // Hammer is ready...!
  18871. Hammer.READY = true;
  18872. }
  18873. /**
  18874. * create new hammer instance
  18875. * all methods should return the instance itself, so it is chainable.
  18876. * @param {HTMLElement} element
  18877. * @param {Object} [options={}]
  18878. * @returns {Hammer.Instance}
  18879. * @constructor
  18880. */
  18881. Hammer.Instance = function(element, options) {
  18882. var self = this;
  18883. // setup HammerJS window events and register all gestures
  18884. // this also sets up the default options
  18885. setup();
  18886. this.element = element;
  18887. // start/stop detection option
  18888. this.enabled = true;
  18889. // merge options
  18890. this.options = Hammer.utils.extend(
  18891. Hammer.utils.extend({}, Hammer.defaults),
  18892. options || {});
  18893. // add some css to the element to prevent the browser from doing its native behavoir
  18894. if(this.options.stop_browser_behavior) {
  18895. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  18896. }
  18897. // start detection on touchstart
  18898. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  18899. if(self.enabled) {
  18900. Hammer.detection.startDetect(self, ev);
  18901. }
  18902. });
  18903. // return instance
  18904. return this;
  18905. };
  18906. Hammer.Instance.prototype = {
  18907. /**
  18908. * bind events to the instance
  18909. * @param {String} gesture
  18910. * @param {Function} handler
  18911. * @returns {Hammer.Instance}
  18912. */
  18913. on: function onEvent(gesture, handler){
  18914. var gestures = gesture.split(' ');
  18915. for(var t=0; t<gestures.length; t++) {
  18916. this.element.addEventListener(gestures[t], handler, false);
  18917. }
  18918. return this;
  18919. },
  18920. /**
  18921. * unbind events to the instance
  18922. * @param {String} gesture
  18923. * @param {Function} handler
  18924. * @returns {Hammer.Instance}
  18925. */
  18926. off: function offEvent(gesture, handler){
  18927. var gestures = gesture.split(' ');
  18928. for(var t=0; t<gestures.length; t++) {
  18929. this.element.removeEventListener(gestures[t], handler, false);
  18930. }
  18931. return this;
  18932. },
  18933. /**
  18934. * trigger gesture event
  18935. * @param {String} gesture
  18936. * @param {Object} eventData
  18937. * @returns {Hammer.Instance}
  18938. */
  18939. trigger: function triggerEvent(gesture, eventData){
  18940. // create DOM event
  18941. var event = Hammer.DOCUMENT.createEvent('Event');
  18942. event.initEvent(gesture, true, true);
  18943. event.gesture = eventData;
  18944. // trigger on the target if it is in the instance element,
  18945. // this is for event delegation tricks
  18946. var element = this.element;
  18947. if(Hammer.utils.hasParent(eventData.target, element)) {
  18948. element = eventData.target;
  18949. }
  18950. element.dispatchEvent(event);
  18951. return this;
  18952. },
  18953. /**
  18954. * enable of disable hammer.js detection
  18955. * @param {Boolean} state
  18956. * @returns {Hammer.Instance}
  18957. */
  18958. enable: function enable(state) {
  18959. this.enabled = state;
  18960. return this;
  18961. }
  18962. };
  18963. /**
  18964. * this holds the last move event,
  18965. * used to fix empty touchend issue
  18966. * see the onTouch event for an explanation
  18967. * @type {Object}
  18968. */
  18969. var last_move_event = null;
  18970. /**
  18971. * when the mouse is hold down, this is true
  18972. * @type {Boolean}
  18973. */
  18974. var enable_detect = false;
  18975. /**
  18976. * when touch events have been fired, this is true
  18977. * @type {Boolean}
  18978. */
  18979. var touch_triggered = false;
  18980. Hammer.event = {
  18981. /**
  18982. * simple addEventListener
  18983. * @param {HTMLElement} element
  18984. * @param {String} type
  18985. * @param {Function} handler
  18986. */
  18987. bindDom: function(element, type, handler) {
  18988. var types = type.split(' ');
  18989. for(var t=0; t<types.length; t++) {
  18990. element.addEventListener(types[t], handler, false);
  18991. }
  18992. },
  18993. /**
  18994. * touch events with mouse fallback
  18995. * @param {HTMLElement} element
  18996. * @param {String} eventType like Hammer.EVENT_MOVE
  18997. * @param {Function} handler
  18998. */
  18999. onTouch: function onTouch(element, eventType, handler) {
  19000. var self = this;
  19001. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  19002. var sourceEventType = ev.type.toLowerCase();
  19003. // onmouseup, but when touchend has been fired we do nothing.
  19004. // this is for touchdevices which also fire a mouseup on touchend
  19005. if(sourceEventType.match(/mouse/) && touch_triggered) {
  19006. return;
  19007. }
  19008. // mousebutton must be down or a touch event
  19009. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  19010. sourceEventType.match(/pointerdown/) || // pointerevents touch
  19011. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  19012. ){
  19013. enable_detect = true;
  19014. }
  19015. // we are in a touch event, set the touch triggered bool to true,
  19016. // this for the conflicts that may occur on ios and android
  19017. if(sourceEventType.match(/touch|pointer/)) {
  19018. touch_triggered = true;
  19019. }
  19020. // count the total touches on the screen
  19021. var count_touches = 0;
  19022. // when touch has been triggered in this detection session
  19023. // and we are now handling a mouse event, we stop that to prevent conflicts
  19024. if(enable_detect) {
  19025. // update pointerevent
  19026. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  19027. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  19028. }
  19029. // touch
  19030. else if(sourceEventType.match(/touch/)) {
  19031. count_touches = ev.touches.length;
  19032. }
  19033. // mouse
  19034. else if(!touch_triggered) {
  19035. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  19036. }
  19037. // if we are in a end event, but when we remove one touch and
  19038. // we still have enough, set eventType to move
  19039. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  19040. eventType = Hammer.EVENT_MOVE;
  19041. }
  19042. // no touches, force the end event
  19043. else if(!count_touches) {
  19044. eventType = Hammer.EVENT_END;
  19045. }
  19046. // because touchend has no touches, and we often want to use these in our gestures,
  19047. // we send the last move event as our eventData in touchend
  19048. if(!count_touches && last_move_event !== null) {
  19049. ev = last_move_event;
  19050. }
  19051. // store the last move event
  19052. else {
  19053. last_move_event = ev;
  19054. }
  19055. // trigger the handler
  19056. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  19057. // remove pointerevent from list
  19058. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  19059. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  19060. }
  19061. }
  19062. //debug(sourceEventType +" "+ eventType);
  19063. // on the end we reset everything
  19064. if(!count_touches) {
  19065. last_move_event = null;
  19066. enable_detect = false;
  19067. touch_triggered = false;
  19068. Hammer.PointerEvent.reset();
  19069. }
  19070. });
  19071. },
  19072. /**
  19073. * we have different events for each device/browser
  19074. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  19075. */
  19076. determineEventTypes: function determineEventTypes() {
  19077. // determine the eventtype we want to set
  19078. var types;
  19079. // pointerEvents magic
  19080. if(Hammer.HAS_POINTEREVENTS) {
  19081. types = Hammer.PointerEvent.getEvents();
  19082. }
  19083. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  19084. else if(Hammer.NO_MOUSEEVENTS) {
  19085. types = [
  19086. 'touchstart',
  19087. 'touchmove',
  19088. 'touchend touchcancel'];
  19089. }
  19090. // for non pointer events browsers and mixed browsers,
  19091. // like chrome on windows8 touch laptop
  19092. else {
  19093. types = [
  19094. 'touchstart mousedown',
  19095. 'touchmove mousemove',
  19096. 'touchend touchcancel mouseup'];
  19097. }
  19098. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  19099. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  19100. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  19101. },
  19102. /**
  19103. * create touchlist depending on the event
  19104. * @param {Object} ev
  19105. * @param {String} eventType used by the fakemultitouch plugin
  19106. */
  19107. getTouchList: function getTouchList(ev/*, eventType*/) {
  19108. // get the fake pointerEvent touchlist
  19109. if(Hammer.HAS_POINTEREVENTS) {
  19110. return Hammer.PointerEvent.getTouchList();
  19111. }
  19112. // get the touchlist
  19113. else if(ev.touches) {
  19114. return ev.touches;
  19115. }
  19116. // make fake touchlist from mouse position
  19117. else {
  19118. return [{
  19119. identifier: 1,
  19120. pageX: ev.pageX,
  19121. pageY: ev.pageY,
  19122. target: ev.target
  19123. }];
  19124. }
  19125. },
  19126. /**
  19127. * collect event data for Hammer js
  19128. * @param {HTMLElement} element
  19129. * @param {String} eventType like Hammer.EVENT_MOVE
  19130. * @param {Object} eventData
  19131. */
  19132. collectEventData: function collectEventData(element, eventType, ev) {
  19133. var touches = this.getTouchList(ev, eventType);
  19134. // find out pointerType
  19135. var pointerType = Hammer.POINTER_TOUCH;
  19136. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  19137. pointerType = Hammer.POINTER_MOUSE;
  19138. }
  19139. return {
  19140. center : Hammer.utils.getCenter(touches),
  19141. timeStamp : new Date().getTime(),
  19142. target : ev.target,
  19143. touches : touches,
  19144. eventType : eventType,
  19145. pointerType : pointerType,
  19146. srcEvent : ev,
  19147. /**
  19148. * prevent the browser default actions
  19149. * mostly used to disable scrolling of the browser
  19150. */
  19151. preventDefault: function() {
  19152. if(this.srcEvent.preventManipulation) {
  19153. this.srcEvent.preventManipulation();
  19154. }
  19155. if(this.srcEvent.preventDefault) {
  19156. this.srcEvent.preventDefault();
  19157. }
  19158. },
  19159. /**
  19160. * stop bubbling the event up to its parents
  19161. */
  19162. stopPropagation: function() {
  19163. this.srcEvent.stopPropagation();
  19164. },
  19165. /**
  19166. * immediately stop gesture detection
  19167. * might be useful after a swipe was detected
  19168. * @return {*}
  19169. */
  19170. stopDetect: function() {
  19171. return Hammer.detection.stopDetect();
  19172. }
  19173. };
  19174. }
  19175. };
  19176. Hammer.PointerEvent = {
  19177. /**
  19178. * holds all pointers
  19179. * @type {Object}
  19180. */
  19181. pointers: {},
  19182. /**
  19183. * get a list of pointers
  19184. * @returns {Array} touchlist
  19185. */
  19186. getTouchList: function() {
  19187. var self = this;
  19188. var touchlist = [];
  19189. // we can use forEach since pointerEvents only is in IE10
  19190. Object.keys(self.pointers).sort().forEach(function(id) {
  19191. touchlist.push(self.pointers[id]);
  19192. });
  19193. return touchlist;
  19194. },
  19195. /**
  19196. * update the position of a pointer
  19197. * @param {String} type Hammer.EVENT_END
  19198. * @param {Object} pointerEvent
  19199. */
  19200. updatePointer: function(type, pointerEvent) {
  19201. if(type == Hammer.EVENT_END) {
  19202. this.pointers = {};
  19203. }
  19204. else {
  19205. pointerEvent.identifier = pointerEvent.pointerId;
  19206. this.pointers[pointerEvent.pointerId] = pointerEvent;
  19207. }
  19208. return Object.keys(this.pointers).length;
  19209. },
  19210. /**
  19211. * check if ev matches pointertype
  19212. * @param {String} pointerType Hammer.POINTER_MOUSE
  19213. * @param {PointerEvent} ev
  19214. */
  19215. matchType: function(pointerType, ev) {
  19216. if(!ev.pointerType) {
  19217. return false;
  19218. }
  19219. var types = {};
  19220. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  19221. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  19222. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  19223. return types[pointerType];
  19224. },
  19225. /**
  19226. * get events
  19227. */
  19228. getEvents: function() {
  19229. return [
  19230. 'pointerdown MSPointerDown',
  19231. 'pointermove MSPointerMove',
  19232. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  19233. ];
  19234. },
  19235. /**
  19236. * reset the list
  19237. */
  19238. reset: function() {
  19239. this.pointers = {};
  19240. }
  19241. };
  19242. Hammer.utils = {
  19243. /**
  19244. * extend method,
  19245. * also used for cloning when dest is an empty object
  19246. * @param {Object} dest
  19247. * @param {Object} src
  19248. * @parm {Boolean} merge do a merge
  19249. * @returns {Object} dest
  19250. */
  19251. extend: function extend(dest, src, merge) {
  19252. for (var key in src) {
  19253. if(dest[key] !== undefined && merge) {
  19254. continue;
  19255. }
  19256. dest[key] = src[key];
  19257. }
  19258. return dest;
  19259. },
  19260. /**
  19261. * find if a node is in the given parent
  19262. * used for event delegation tricks
  19263. * @param {HTMLElement} node
  19264. * @param {HTMLElement} parent
  19265. * @returns {boolean} has_parent
  19266. */
  19267. hasParent: function(node, parent) {
  19268. while(node){
  19269. if(node == parent) {
  19270. return true;
  19271. }
  19272. node = node.parentNode;
  19273. }
  19274. return false;
  19275. },
  19276. /**
  19277. * get the center of all the touches
  19278. * @param {Array} touches
  19279. * @returns {Object} center
  19280. */
  19281. getCenter: function getCenter(touches) {
  19282. var valuesX = [], valuesY = [];
  19283. for(var t= 0,len=touches.length; t<len; t++) {
  19284. valuesX.push(touches[t].pageX);
  19285. valuesY.push(touches[t].pageY);
  19286. }
  19287. return {
  19288. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  19289. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  19290. };
  19291. },
  19292. /**
  19293. * calculate the velocity between two points
  19294. * @param {Number} delta_time
  19295. * @param {Number} delta_x
  19296. * @param {Number} delta_y
  19297. * @returns {Object} velocity
  19298. */
  19299. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  19300. return {
  19301. x: Math.abs(delta_x / delta_time) || 0,
  19302. y: Math.abs(delta_y / delta_time) || 0
  19303. };
  19304. },
  19305. /**
  19306. * calculate the angle between two coordinates
  19307. * @param {Touch} touch1
  19308. * @param {Touch} touch2
  19309. * @returns {Number} angle
  19310. */
  19311. getAngle: function getAngle(touch1, touch2) {
  19312. var y = touch2.pageY - touch1.pageY,
  19313. x = touch2.pageX - touch1.pageX;
  19314. return Math.atan2(y, x) * 180 / Math.PI;
  19315. },
  19316. /**
  19317. * angle to direction define
  19318. * @param {Touch} touch1
  19319. * @param {Touch} touch2
  19320. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  19321. */
  19322. getDirection: function getDirection(touch1, touch2) {
  19323. var x = Math.abs(touch1.pageX - touch2.pageX),
  19324. y = Math.abs(touch1.pageY - touch2.pageY);
  19325. if(x >= y) {
  19326. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  19327. }
  19328. else {
  19329. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  19330. }
  19331. },
  19332. /**
  19333. * calculate the distance between two touches
  19334. * @param {Touch} touch1
  19335. * @param {Touch} touch2
  19336. * @returns {Number} distance
  19337. */
  19338. getDistance: function getDistance(touch1, touch2) {
  19339. var x = touch2.pageX - touch1.pageX,
  19340. y = touch2.pageY - touch1.pageY;
  19341. return Math.sqrt((x*x) + (y*y));
  19342. },
  19343. /**
  19344. * calculate the scale factor between two touchLists (fingers)
  19345. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  19346. * @param {Array} start
  19347. * @param {Array} end
  19348. * @returns {Number} scale
  19349. */
  19350. getScale: function getScale(start, end) {
  19351. // need two fingers...
  19352. if(start.length >= 2 && end.length >= 2) {
  19353. return this.getDistance(end[0], end[1]) /
  19354. this.getDistance(start[0], start[1]);
  19355. }
  19356. return 1;
  19357. },
  19358. /**
  19359. * calculate the rotation degrees between two touchLists (fingers)
  19360. * @param {Array} start
  19361. * @param {Array} end
  19362. * @returns {Number} rotation
  19363. */
  19364. getRotation: function getRotation(start, end) {
  19365. // need two fingers
  19366. if(start.length >= 2 && end.length >= 2) {
  19367. return this.getAngle(end[1], end[0]) -
  19368. this.getAngle(start[1], start[0]);
  19369. }
  19370. return 0;
  19371. },
  19372. /**
  19373. * boolean if the direction is vertical
  19374. * @param {String} direction
  19375. * @returns {Boolean} is_vertical
  19376. */
  19377. isVertical: function isVertical(direction) {
  19378. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  19379. },
  19380. /**
  19381. * stop browser default behavior with css props
  19382. * @param {HtmlElement} element
  19383. * @param {Object} css_props
  19384. */
  19385. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  19386. var prop,
  19387. vendors = ['webkit','khtml','moz','ms','o',''];
  19388. if(!css_props || !element.style) {
  19389. return;
  19390. }
  19391. // with css properties for modern browsers
  19392. for(var i = 0; i < vendors.length; i++) {
  19393. for(var p in css_props) {
  19394. if(css_props.hasOwnProperty(p)) {
  19395. prop = p;
  19396. // vender prefix at the property
  19397. if(vendors[i]) {
  19398. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  19399. }
  19400. // set the style
  19401. element.style[prop] = css_props[p];
  19402. }
  19403. }
  19404. }
  19405. // also the disable onselectstart
  19406. if(css_props.userSelect == 'none') {
  19407. element.onselectstart = function() {
  19408. return false;
  19409. };
  19410. }
  19411. }
  19412. };
  19413. Hammer.detection = {
  19414. // contains all registred Hammer.gestures in the correct order
  19415. gestures: [],
  19416. // data of the current Hammer.gesture detection session
  19417. current: null,
  19418. // the previous Hammer.gesture session data
  19419. // is a full clone of the previous gesture.current object
  19420. previous: null,
  19421. // when this becomes true, no gestures are fired
  19422. stopped: false,
  19423. /**
  19424. * start Hammer.gesture detection
  19425. * @param {Hammer.Instance} inst
  19426. * @param {Object} eventData
  19427. */
  19428. startDetect: function startDetect(inst, eventData) {
  19429. // already busy with a Hammer.gesture detection on an element
  19430. if(this.current) {
  19431. return;
  19432. }
  19433. this.stopped = false;
  19434. this.current = {
  19435. inst : inst, // reference to HammerInstance we're working for
  19436. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  19437. lastEvent : false, // last eventData
  19438. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  19439. };
  19440. this.detect(eventData);
  19441. },
  19442. /**
  19443. * Hammer.gesture detection
  19444. * @param {Object} eventData
  19445. * @param {Object} eventData
  19446. */
  19447. detect: function detect(eventData) {
  19448. if(!this.current || this.stopped) {
  19449. return;
  19450. }
  19451. // extend event data with calculations about scale, distance etc
  19452. eventData = this.extendEventData(eventData);
  19453. // instance options
  19454. var inst_options = this.current.inst.options;
  19455. // call Hammer.gesture handlers
  19456. for(var g=0,len=this.gestures.length; g<len; g++) {
  19457. var gesture = this.gestures[g];
  19458. // only when the instance options have enabled this gesture
  19459. if(!this.stopped && inst_options[gesture.name] !== false) {
  19460. // if a handler returns false, we stop with the detection
  19461. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  19462. this.stopDetect();
  19463. break;
  19464. }
  19465. }
  19466. }
  19467. // store as previous event event
  19468. if(this.current) {
  19469. this.current.lastEvent = eventData;
  19470. }
  19471. // endevent, but not the last touch, so dont stop
  19472. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  19473. this.stopDetect();
  19474. }
  19475. return eventData;
  19476. },
  19477. /**
  19478. * clear the Hammer.gesture vars
  19479. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  19480. * to stop other Hammer.gestures from being fired
  19481. */
  19482. stopDetect: function stopDetect() {
  19483. // clone current data to the store as the previous gesture
  19484. // used for the double tap gesture, since this is an other gesture detect session
  19485. this.previous = Hammer.utils.extend({}, this.current);
  19486. // reset the current
  19487. this.current = null;
  19488. // stopped!
  19489. this.stopped = true;
  19490. },
  19491. /**
  19492. * extend eventData for Hammer.gestures
  19493. * @param {Object} ev
  19494. * @returns {Object} ev
  19495. */
  19496. extendEventData: function extendEventData(ev) {
  19497. var startEv = this.current.startEvent;
  19498. // if the touches change, set the new touches over the startEvent touches
  19499. // this because touchevents don't have all the touches on touchstart, or the
  19500. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  19501. // but, sometimes it happens that both fingers are touching at the EXACT same time
  19502. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  19503. // extend 1 level deep to get the touchlist with the touch objects
  19504. startEv.touches = [];
  19505. for(var i=0,len=ev.touches.length; i<len; i++) {
  19506. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  19507. }
  19508. }
  19509. var delta_time = ev.timeStamp - startEv.timeStamp,
  19510. delta_x = ev.center.pageX - startEv.center.pageX,
  19511. delta_y = ev.center.pageY - startEv.center.pageY,
  19512. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  19513. Hammer.utils.extend(ev, {
  19514. deltaTime : delta_time,
  19515. deltaX : delta_x,
  19516. deltaY : delta_y,
  19517. velocityX : velocity.x,
  19518. velocityY : velocity.y,
  19519. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  19520. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  19521. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  19522. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  19523. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  19524. startEvent : startEv
  19525. });
  19526. return ev;
  19527. },
  19528. /**
  19529. * register new gesture
  19530. * @param {Object} gesture object, see gestures.js for documentation
  19531. * @returns {Array} gestures
  19532. */
  19533. register: function register(gesture) {
  19534. // add an enable gesture options if there is no given
  19535. var options = gesture.defaults || {};
  19536. if(options[gesture.name] === undefined) {
  19537. options[gesture.name] = true;
  19538. }
  19539. // extend Hammer default options with the Hammer.gesture options
  19540. Hammer.utils.extend(Hammer.defaults, options, true);
  19541. // set its index
  19542. gesture.index = gesture.index || 1000;
  19543. // add Hammer.gesture to the list
  19544. this.gestures.push(gesture);
  19545. // sort the list by index
  19546. this.gestures.sort(function(a, b) {
  19547. if (a.index < b.index) {
  19548. return -1;
  19549. }
  19550. if (a.index > b.index) {
  19551. return 1;
  19552. }
  19553. return 0;
  19554. });
  19555. return this.gestures;
  19556. }
  19557. };
  19558. Hammer.gestures = Hammer.gestures || {};
  19559. /**
  19560. * Custom gestures
  19561. * ==============================
  19562. *
  19563. * Gesture object
  19564. * --------------------
  19565. * The object structure of a gesture:
  19566. *
  19567. * { name: 'mygesture',
  19568. * index: 1337,
  19569. * defaults: {
  19570. * mygesture_option: true
  19571. * }
  19572. * handler: function(type, ev, inst) {
  19573. * // trigger gesture event
  19574. * inst.trigger(this.name, ev);
  19575. * }
  19576. * }
  19577. * @param {String} name
  19578. * this should be the name of the gesture, lowercase
  19579. * it is also being used to disable/enable the gesture per instance config.
  19580. *
  19581. * @param {Number} [index=1000]
  19582. * the index of the gesture, where it is going to be in the stack of gestures detection
  19583. * like when you build an gesture that depends on the drag gesture, it is a good
  19584. * idea to place it after the index of the drag gesture.
  19585. *
  19586. * @param {Object} [defaults={}]
  19587. * the default settings of the gesture. these are added to the instance settings,
  19588. * and can be overruled per instance. you can also add the name of the gesture,
  19589. * but this is also added by default (and set to true).
  19590. *
  19591. * @param {Function} handler
  19592. * this handles the gesture detection of your custom gesture and receives the
  19593. * following arguments:
  19594. *
  19595. * @param {Object} eventData
  19596. * event data containing the following properties:
  19597. * timeStamp {Number} time the event occurred
  19598. * target {HTMLElement} target element
  19599. * touches {Array} touches (fingers, pointers, mouse) on the screen
  19600. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  19601. * center {Object} center position of the touches. contains pageX and pageY
  19602. * deltaTime {Number} the total time of the touches in the screen
  19603. * deltaX {Number} the delta on x axis we haved moved
  19604. * deltaY {Number} the delta on y axis we haved moved
  19605. * velocityX {Number} the velocity on the x
  19606. * velocityY {Number} the velocity on y
  19607. * angle {Number} the angle we are moving
  19608. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  19609. * distance {Number} the distance we haved moved
  19610. * scale {Number} scaling of the touches, needs 2 touches
  19611. * rotation {Number} rotation of the touches, needs 2 touches *
  19612. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  19613. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  19614. * startEvent {Object} contains the same properties as above,
  19615. * but from the first touch. this is used to calculate
  19616. * distances, deltaTime, scaling etc
  19617. *
  19618. * @param {Hammer.Instance} inst
  19619. * the instance we are doing the detection for. you can get the options from
  19620. * the inst.options object and trigger the gesture event by calling inst.trigger
  19621. *
  19622. *
  19623. * Handle gestures
  19624. * --------------------
  19625. * inside the handler you can get/set Hammer.detection.current. This is the current
  19626. * detection session. It has the following properties
  19627. * @param {String} name
  19628. * contains the name of the gesture we have detected. it has not a real function,
  19629. * only to check in other gestures if something is detected.
  19630. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  19631. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  19632. *
  19633. * @readonly
  19634. * @param {Hammer.Instance} inst
  19635. * the instance we do the detection for
  19636. *
  19637. * @readonly
  19638. * @param {Object} startEvent
  19639. * contains the properties of the first gesture detection in this session.
  19640. * Used for calculations about timing, distance, etc.
  19641. *
  19642. * @readonly
  19643. * @param {Object} lastEvent
  19644. * contains all the properties of the last gesture detect in this session.
  19645. *
  19646. * after the gesture detection session has been completed (user has released the screen)
  19647. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  19648. * this is usefull for gestures like doubletap, where you need to know if the
  19649. * previous gesture was a tap
  19650. *
  19651. * options that have been set by the instance can be received by calling inst.options
  19652. *
  19653. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  19654. * The first param is the name of your gesture, the second the event argument
  19655. *
  19656. *
  19657. * Register gestures
  19658. * --------------------
  19659. * When an gesture is added to the Hammer.gestures object, it is auto registered
  19660. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  19661. * manually and pass your gesture object as a param
  19662. *
  19663. */
  19664. /**
  19665. * Hold
  19666. * Touch stays at the same place for x time
  19667. * @events hold
  19668. */
  19669. Hammer.gestures.Hold = {
  19670. name: 'hold',
  19671. index: 10,
  19672. defaults: {
  19673. hold_timeout : 500,
  19674. hold_threshold : 1
  19675. },
  19676. timer: null,
  19677. handler: function holdGesture(ev, inst) {
  19678. switch(ev.eventType) {
  19679. case Hammer.EVENT_START:
  19680. // clear any running timers
  19681. clearTimeout(this.timer);
  19682. // set the gesture so we can check in the timeout if it still is
  19683. Hammer.detection.current.name = this.name;
  19684. // set timer and if after the timeout it still is hold,
  19685. // we trigger the hold event
  19686. this.timer = setTimeout(function() {
  19687. if(Hammer.detection.current.name == 'hold') {
  19688. inst.trigger('hold', ev);
  19689. }
  19690. }, inst.options.hold_timeout);
  19691. break;
  19692. // when you move or end we clear the timer
  19693. case Hammer.EVENT_MOVE:
  19694. if(ev.distance > inst.options.hold_threshold) {
  19695. clearTimeout(this.timer);
  19696. }
  19697. break;
  19698. case Hammer.EVENT_END:
  19699. clearTimeout(this.timer);
  19700. break;
  19701. }
  19702. }
  19703. };
  19704. /**
  19705. * Tap/DoubleTap
  19706. * Quick touch at a place or double at the same place
  19707. * @events tap, doubletap
  19708. */
  19709. Hammer.gestures.Tap = {
  19710. name: 'tap',
  19711. index: 100,
  19712. defaults: {
  19713. tap_max_touchtime : 250,
  19714. tap_max_distance : 10,
  19715. tap_always : true,
  19716. doubletap_distance : 20,
  19717. doubletap_interval : 300
  19718. },
  19719. handler: function tapGesture(ev, inst) {
  19720. if(ev.eventType == Hammer.EVENT_END) {
  19721. // previous gesture, for the double tap since these are two different gesture detections
  19722. var prev = Hammer.detection.previous,
  19723. did_doubletap = false;
  19724. // when the touchtime is higher then the max touch time
  19725. // or when the moving distance is too much
  19726. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  19727. ev.distance > inst.options.tap_max_distance) {
  19728. return;
  19729. }
  19730. // check if double tap
  19731. if(prev && prev.name == 'tap' &&
  19732. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  19733. ev.distance < inst.options.doubletap_distance) {
  19734. inst.trigger('doubletap', ev);
  19735. did_doubletap = true;
  19736. }
  19737. // do a single tap
  19738. if(!did_doubletap || inst.options.tap_always) {
  19739. Hammer.detection.current.name = 'tap';
  19740. inst.trigger(Hammer.detection.current.name, ev);
  19741. }
  19742. }
  19743. }
  19744. };
  19745. /**
  19746. * Swipe
  19747. * triggers swipe events when the end velocity is above the threshold
  19748. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  19749. */
  19750. Hammer.gestures.Swipe = {
  19751. name: 'swipe',
  19752. index: 40,
  19753. defaults: {
  19754. // set 0 for unlimited, but this can conflict with transform
  19755. swipe_max_touches : 1,
  19756. swipe_velocity : 0.7
  19757. },
  19758. handler: function swipeGesture(ev, inst) {
  19759. if(ev.eventType == Hammer.EVENT_END) {
  19760. // max touches
  19761. if(inst.options.swipe_max_touches > 0 &&
  19762. ev.touches.length > inst.options.swipe_max_touches) {
  19763. return;
  19764. }
  19765. // when the distance we moved is too small we skip this gesture
  19766. // or we can be already in dragging
  19767. if(ev.velocityX > inst.options.swipe_velocity ||
  19768. ev.velocityY > inst.options.swipe_velocity) {
  19769. // trigger swipe events
  19770. inst.trigger(this.name, ev);
  19771. inst.trigger(this.name + ev.direction, ev);
  19772. }
  19773. }
  19774. }
  19775. };
  19776. /**
  19777. * Drag
  19778. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  19779. * moving left and right is a good practice. When all the drag events are blocking
  19780. * you disable scrolling on that area.
  19781. * @events drag, drapleft, dragright, dragup, dragdown
  19782. */
  19783. Hammer.gestures.Drag = {
  19784. name: 'drag',
  19785. index: 50,
  19786. defaults: {
  19787. drag_min_distance : 10,
  19788. // set 0 for unlimited, but this can conflict with transform
  19789. drag_max_touches : 1,
  19790. // prevent default browser behavior when dragging occurs
  19791. // be careful with it, it makes the element a blocking element
  19792. // when you are using the drag gesture, it is a good practice to set this true
  19793. drag_block_horizontal : false,
  19794. drag_block_vertical : false,
  19795. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  19796. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  19797. drag_lock_to_axis : false,
  19798. // drag lock only kicks in when distance > drag_lock_min_distance
  19799. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  19800. drag_lock_min_distance : 25
  19801. },
  19802. triggered: false,
  19803. handler: function dragGesture(ev, inst) {
  19804. // current gesture isnt drag, but dragged is true
  19805. // this means an other gesture is busy. now call dragend
  19806. if(Hammer.detection.current.name != this.name && this.triggered) {
  19807. inst.trigger(this.name +'end', ev);
  19808. this.triggered = false;
  19809. return;
  19810. }
  19811. // max touches
  19812. if(inst.options.drag_max_touches > 0 &&
  19813. ev.touches.length > inst.options.drag_max_touches) {
  19814. return;
  19815. }
  19816. switch(ev.eventType) {
  19817. case Hammer.EVENT_START:
  19818. this.triggered = false;
  19819. break;
  19820. case Hammer.EVENT_MOVE:
  19821. // when the distance we moved is too small we skip this gesture
  19822. // or we can be already in dragging
  19823. if(ev.distance < inst.options.drag_min_distance &&
  19824. Hammer.detection.current.name != this.name) {
  19825. return;
  19826. }
  19827. // we are dragging!
  19828. Hammer.detection.current.name = this.name;
  19829. // lock drag to axis?
  19830. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  19831. ev.drag_locked_to_axis = true;
  19832. }
  19833. var last_direction = Hammer.detection.current.lastEvent.direction;
  19834. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  19835. // keep direction on the axis that the drag gesture started on
  19836. if(Hammer.utils.isVertical(last_direction)) {
  19837. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  19838. }
  19839. else {
  19840. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  19841. }
  19842. }
  19843. // first time, trigger dragstart event
  19844. if(!this.triggered) {
  19845. inst.trigger(this.name +'start', ev);
  19846. this.triggered = true;
  19847. }
  19848. // trigger normal event
  19849. inst.trigger(this.name, ev);
  19850. // direction event, like dragdown
  19851. inst.trigger(this.name + ev.direction, ev);
  19852. // block the browser events
  19853. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  19854. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  19855. ev.preventDefault();
  19856. }
  19857. break;
  19858. case Hammer.EVENT_END:
  19859. // trigger dragend
  19860. if(this.triggered) {
  19861. inst.trigger(this.name +'end', ev);
  19862. }
  19863. this.triggered = false;
  19864. break;
  19865. }
  19866. }
  19867. };
  19868. /**
  19869. * Transform
  19870. * User want to scale or rotate with 2 fingers
  19871. * @events transform, pinch, pinchin, pinchout, rotate
  19872. */
  19873. Hammer.gestures.Transform = {
  19874. name: 'transform',
  19875. index: 45,
  19876. defaults: {
  19877. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  19878. transform_min_scale : 0.01,
  19879. // rotation in degrees
  19880. transform_min_rotation : 1,
  19881. // prevent default browser behavior when two touches are on the screen
  19882. // but it makes the element a blocking element
  19883. // when you are using the transform gesture, it is a good practice to set this true
  19884. transform_always_block : false
  19885. },
  19886. triggered: false,
  19887. handler: function transformGesture(ev, inst) {
  19888. // current gesture isnt drag, but dragged is true
  19889. // this means an other gesture is busy. now call dragend
  19890. if(Hammer.detection.current.name != this.name && this.triggered) {
  19891. inst.trigger(this.name +'end', ev);
  19892. this.triggered = false;
  19893. return;
  19894. }
  19895. // atleast multitouch
  19896. if(ev.touches.length < 2) {
  19897. return;
  19898. }
  19899. // prevent default when two fingers are on the screen
  19900. if(inst.options.transform_always_block) {
  19901. ev.preventDefault();
  19902. }
  19903. switch(ev.eventType) {
  19904. case Hammer.EVENT_START:
  19905. this.triggered = false;
  19906. break;
  19907. case Hammer.EVENT_MOVE:
  19908. var scale_threshold = Math.abs(1-ev.scale);
  19909. var rotation_threshold = Math.abs(ev.rotation);
  19910. // when the distance we moved is too small we skip this gesture
  19911. // or we can be already in dragging
  19912. if(scale_threshold < inst.options.transform_min_scale &&
  19913. rotation_threshold < inst.options.transform_min_rotation) {
  19914. return;
  19915. }
  19916. // we are transforming!
  19917. Hammer.detection.current.name = this.name;
  19918. // first time, trigger dragstart event
  19919. if(!this.triggered) {
  19920. inst.trigger(this.name +'start', ev);
  19921. this.triggered = true;
  19922. }
  19923. inst.trigger(this.name, ev); // basic transform event
  19924. // trigger rotate event
  19925. if(rotation_threshold > inst.options.transform_min_rotation) {
  19926. inst.trigger('rotate', ev);
  19927. }
  19928. // trigger pinch event
  19929. if(scale_threshold > inst.options.transform_min_scale) {
  19930. inst.trigger('pinch', ev);
  19931. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  19932. }
  19933. break;
  19934. case Hammer.EVENT_END:
  19935. // trigger dragend
  19936. if(this.triggered) {
  19937. inst.trigger(this.name +'end', ev);
  19938. }
  19939. this.triggered = false;
  19940. break;
  19941. }
  19942. }
  19943. };
  19944. /**
  19945. * Touch
  19946. * Called as first, tells the user has touched the screen
  19947. * @events touch
  19948. */
  19949. Hammer.gestures.Touch = {
  19950. name: 'touch',
  19951. index: -Infinity,
  19952. defaults: {
  19953. // call preventDefault at touchstart, and makes the element blocking by
  19954. // disabling the scrolling of the page, but it improves gestures like
  19955. // transforming and dragging.
  19956. // be careful with using this, it can be very annoying for users to be stuck
  19957. // on the page
  19958. prevent_default: false,
  19959. // disable mouse events, so only touch (or pen!) input triggers events
  19960. prevent_mouseevents: false
  19961. },
  19962. handler: function touchGesture(ev, inst) {
  19963. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  19964. ev.stopDetect();
  19965. return;
  19966. }
  19967. if(inst.options.prevent_default) {
  19968. ev.preventDefault();
  19969. }
  19970. if(ev.eventType == Hammer.EVENT_START) {
  19971. inst.trigger(this.name, ev);
  19972. }
  19973. }
  19974. };
  19975. /**
  19976. * Release
  19977. * Called as last, tells the user has released the screen
  19978. * @events release
  19979. */
  19980. Hammer.gestures.Release = {
  19981. name: 'release',
  19982. index: Infinity,
  19983. handler: function releaseGesture(ev, inst) {
  19984. if(ev.eventType == Hammer.EVENT_END) {
  19985. inst.trigger(this.name, ev);
  19986. }
  19987. }
  19988. };
  19989. // node export
  19990. if(typeof module === 'object' && typeof module.exports === 'object'){
  19991. module.exports = Hammer;
  19992. }
  19993. // just window export
  19994. else {
  19995. window.Hammer = Hammer;
  19996. // requireJS module definition
  19997. if(typeof window.define === 'function' && window.define.amd) {
  19998. window.define('hammer', [], function() {
  19999. return Hammer;
  20000. });
  20001. }
  20002. }
  20003. })(this);
  20004. },{}],4:[function(require,module,exports){
  20005. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  20006. //! version : 2.6.0
  20007. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  20008. //! license : MIT
  20009. //! momentjs.com
  20010. (function (undefined) {
  20011. /************************************
  20012. Constants
  20013. ************************************/
  20014. var moment,
  20015. VERSION = "2.6.0",
  20016. // the global-scope this is NOT the global object in Node.js
  20017. globalScope = typeof global !== 'undefined' ? global : this,
  20018. oldGlobalMoment,
  20019. round = Math.round,
  20020. i,
  20021. YEAR = 0,
  20022. MONTH = 1,
  20023. DATE = 2,
  20024. HOUR = 3,
  20025. MINUTE = 4,
  20026. SECOND = 5,
  20027. MILLISECOND = 6,
  20028. // internal storage for language config files
  20029. languages = {},
  20030. // moment internal properties
  20031. momentProperties = {
  20032. _isAMomentObject: null,
  20033. _i : null,
  20034. _f : null,
  20035. _l : null,
  20036. _strict : null,
  20037. _isUTC : null,
  20038. _offset : null, // optional. Combine with _isUTC
  20039. _pf : null,
  20040. _lang : null // optional
  20041. },
  20042. // check for nodeJS
  20043. hasModule = (typeof module !== 'undefined' && module.exports),
  20044. // ASP.NET json date format regex
  20045. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  20046. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  20047. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  20048. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  20049. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  20050. // format tokens
  20051. 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,
  20052. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  20053. // parsing token regexes
  20054. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  20055. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  20056. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  20057. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  20058. parseTokenDigits = /\d+/, // nonzero number of digits
  20059. 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.
  20060. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  20061. parseTokenT = /T/i, // T (ISO separator)
  20062. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  20063. parseTokenOrdinal = /\d{1,2}/,
  20064. //strict parsing regexes
  20065. parseTokenOneDigit = /\d/, // 0 - 9
  20066. parseTokenTwoDigits = /\d\d/, // 00 - 99
  20067. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  20068. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  20069. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  20070. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  20071. // iso 8601 regex
  20072. // 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)
  20073. 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)?)?$/,
  20074. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  20075. isoDates = [
  20076. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  20077. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  20078. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  20079. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  20080. ['YYYY-DDD', /\d{4}-\d{3}/]
  20081. ],
  20082. // iso time formats and regexes
  20083. isoTimes = [
  20084. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  20085. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  20086. ['HH:mm', /(T| )\d\d:\d\d/],
  20087. ['HH', /(T| )\d\d/]
  20088. ],
  20089. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  20090. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  20091. // getter and setter names
  20092. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  20093. unitMillisecondFactors = {
  20094. 'Milliseconds' : 1,
  20095. 'Seconds' : 1e3,
  20096. 'Minutes' : 6e4,
  20097. 'Hours' : 36e5,
  20098. 'Days' : 864e5,
  20099. 'Months' : 2592e6,
  20100. 'Years' : 31536e6
  20101. },
  20102. unitAliases = {
  20103. ms : 'millisecond',
  20104. s : 'second',
  20105. m : 'minute',
  20106. h : 'hour',
  20107. d : 'day',
  20108. D : 'date',
  20109. w : 'week',
  20110. W : 'isoWeek',
  20111. M : 'month',
  20112. Q : 'quarter',
  20113. y : 'year',
  20114. DDD : 'dayOfYear',
  20115. e : 'weekday',
  20116. E : 'isoWeekday',
  20117. gg: 'weekYear',
  20118. GG: 'isoWeekYear'
  20119. },
  20120. camelFunctions = {
  20121. dayofyear : 'dayOfYear',
  20122. isoweekday : 'isoWeekday',
  20123. isoweek : 'isoWeek',
  20124. weekyear : 'weekYear',
  20125. isoweekyear : 'isoWeekYear'
  20126. },
  20127. // format function strings
  20128. formatFunctions = {},
  20129. // tokens to ordinalize and pad
  20130. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  20131. paddedTokens = 'M D H h m s w W'.split(' '),
  20132. formatTokenFunctions = {
  20133. M : function () {
  20134. return this.month() + 1;
  20135. },
  20136. MMM : function (format) {
  20137. return this.lang().monthsShort(this, format);
  20138. },
  20139. MMMM : function (format) {
  20140. return this.lang().months(this, format);
  20141. },
  20142. D : function () {
  20143. return this.date();
  20144. },
  20145. DDD : function () {
  20146. return this.dayOfYear();
  20147. },
  20148. d : function () {
  20149. return this.day();
  20150. },
  20151. dd : function (format) {
  20152. return this.lang().weekdaysMin(this, format);
  20153. },
  20154. ddd : function (format) {
  20155. return this.lang().weekdaysShort(this, format);
  20156. },
  20157. dddd : function (format) {
  20158. return this.lang().weekdays(this, format);
  20159. },
  20160. w : function () {
  20161. return this.week();
  20162. },
  20163. W : function () {
  20164. return this.isoWeek();
  20165. },
  20166. YY : function () {
  20167. return leftZeroFill(this.year() % 100, 2);
  20168. },
  20169. YYYY : function () {
  20170. return leftZeroFill(this.year(), 4);
  20171. },
  20172. YYYYY : function () {
  20173. return leftZeroFill(this.year(), 5);
  20174. },
  20175. YYYYYY : function () {
  20176. var y = this.year(), sign = y >= 0 ? '+' : '-';
  20177. return sign + leftZeroFill(Math.abs(y), 6);
  20178. },
  20179. gg : function () {
  20180. return leftZeroFill(this.weekYear() % 100, 2);
  20181. },
  20182. gggg : function () {
  20183. return leftZeroFill(this.weekYear(), 4);
  20184. },
  20185. ggggg : function () {
  20186. return leftZeroFill(this.weekYear(), 5);
  20187. },
  20188. GG : function () {
  20189. return leftZeroFill(this.isoWeekYear() % 100, 2);
  20190. },
  20191. GGGG : function () {
  20192. return leftZeroFill(this.isoWeekYear(), 4);
  20193. },
  20194. GGGGG : function () {
  20195. return leftZeroFill(this.isoWeekYear(), 5);
  20196. },
  20197. e : function () {
  20198. return this.weekday();
  20199. },
  20200. E : function () {
  20201. return this.isoWeekday();
  20202. },
  20203. a : function () {
  20204. return this.lang().meridiem(this.hours(), this.minutes(), true);
  20205. },
  20206. A : function () {
  20207. return this.lang().meridiem(this.hours(), this.minutes(), false);
  20208. },
  20209. H : function () {
  20210. return this.hours();
  20211. },
  20212. h : function () {
  20213. return this.hours() % 12 || 12;
  20214. },
  20215. m : function () {
  20216. return this.minutes();
  20217. },
  20218. s : function () {
  20219. return this.seconds();
  20220. },
  20221. S : function () {
  20222. return toInt(this.milliseconds() / 100);
  20223. },
  20224. SS : function () {
  20225. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  20226. },
  20227. SSS : function () {
  20228. return leftZeroFill(this.milliseconds(), 3);
  20229. },
  20230. SSSS : function () {
  20231. return leftZeroFill(this.milliseconds(), 3);
  20232. },
  20233. Z : function () {
  20234. var a = -this.zone(),
  20235. b = "+";
  20236. if (a < 0) {
  20237. a = -a;
  20238. b = "-";
  20239. }
  20240. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  20241. },
  20242. ZZ : function () {
  20243. var a = -this.zone(),
  20244. b = "+";
  20245. if (a < 0) {
  20246. a = -a;
  20247. b = "-";
  20248. }
  20249. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  20250. },
  20251. z : function () {
  20252. return this.zoneAbbr();
  20253. },
  20254. zz : function () {
  20255. return this.zoneName();
  20256. },
  20257. X : function () {
  20258. return this.unix();
  20259. },
  20260. Q : function () {
  20261. return this.quarter();
  20262. }
  20263. },
  20264. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  20265. function defaultParsingFlags() {
  20266. // We need to deep clone this object, and es5 standard is not very
  20267. // helpful.
  20268. return {
  20269. empty : false,
  20270. unusedTokens : [],
  20271. unusedInput : [],
  20272. overflow : -2,
  20273. charsLeftOver : 0,
  20274. nullInput : false,
  20275. invalidMonth : null,
  20276. invalidFormat : false,
  20277. userInvalidated : false,
  20278. iso: false
  20279. };
  20280. }
  20281. function deprecate(msg, fn) {
  20282. var firstTime = true;
  20283. function printMsg() {
  20284. if (moment.suppressDeprecationWarnings === false &&
  20285. typeof console !== 'undefined' && console.warn) {
  20286. console.warn("Deprecation warning: " + msg);
  20287. }
  20288. }
  20289. return extend(function () {
  20290. if (firstTime) {
  20291. printMsg();
  20292. firstTime = false;
  20293. }
  20294. return fn.apply(this, arguments);
  20295. }, fn);
  20296. }
  20297. function padToken(func, count) {
  20298. return function (a) {
  20299. return leftZeroFill(func.call(this, a), count);
  20300. };
  20301. }
  20302. function ordinalizeToken(func, period) {
  20303. return function (a) {
  20304. return this.lang().ordinal(func.call(this, a), period);
  20305. };
  20306. }
  20307. while (ordinalizeTokens.length) {
  20308. i = ordinalizeTokens.pop();
  20309. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  20310. }
  20311. while (paddedTokens.length) {
  20312. i = paddedTokens.pop();
  20313. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  20314. }
  20315. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  20316. /************************************
  20317. Constructors
  20318. ************************************/
  20319. function Language() {
  20320. }
  20321. // Moment prototype object
  20322. function Moment(config) {
  20323. checkOverflow(config);
  20324. extend(this, config);
  20325. }
  20326. // Duration Constructor
  20327. function Duration(duration) {
  20328. var normalizedInput = normalizeObjectUnits(duration),
  20329. years = normalizedInput.year || 0,
  20330. quarters = normalizedInput.quarter || 0,
  20331. months = normalizedInput.month || 0,
  20332. weeks = normalizedInput.week || 0,
  20333. days = normalizedInput.day || 0,
  20334. hours = normalizedInput.hour || 0,
  20335. minutes = normalizedInput.minute || 0,
  20336. seconds = normalizedInput.second || 0,
  20337. milliseconds = normalizedInput.millisecond || 0;
  20338. // representation for dateAddRemove
  20339. this._milliseconds = +milliseconds +
  20340. seconds * 1e3 + // 1000
  20341. minutes * 6e4 + // 1000 * 60
  20342. hours * 36e5; // 1000 * 60 * 60
  20343. // Because of dateAddRemove treats 24 hours as different from a
  20344. // day when working around DST, we need to store them separately
  20345. this._days = +days +
  20346. weeks * 7;
  20347. // It is impossible translate months into days without knowing
  20348. // which months you are are talking about, so we have to store
  20349. // it separately.
  20350. this._months = +months +
  20351. quarters * 3 +
  20352. years * 12;
  20353. this._data = {};
  20354. this._bubble();
  20355. }
  20356. /************************************
  20357. Helpers
  20358. ************************************/
  20359. function extend(a, b) {
  20360. for (var i in b) {
  20361. if (b.hasOwnProperty(i)) {
  20362. a[i] = b[i];
  20363. }
  20364. }
  20365. if (b.hasOwnProperty("toString")) {
  20366. a.toString = b.toString;
  20367. }
  20368. if (b.hasOwnProperty("valueOf")) {
  20369. a.valueOf = b.valueOf;
  20370. }
  20371. return a;
  20372. }
  20373. function cloneMoment(m) {
  20374. var result = {}, i;
  20375. for (i in m) {
  20376. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  20377. result[i] = m[i];
  20378. }
  20379. }
  20380. return result;
  20381. }
  20382. function absRound(number) {
  20383. if (number < 0) {
  20384. return Math.ceil(number);
  20385. } else {
  20386. return Math.floor(number);
  20387. }
  20388. }
  20389. // left zero fill a number
  20390. // see http://jsperf.com/left-zero-filling for performance comparison
  20391. function leftZeroFill(number, targetLength, forceSign) {
  20392. var output = '' + Math.abs(number),
  20393. sign = number >= 0;
  20394. while (output.length < targetLength) {
  20395. output = '0' + output;
  20396. }
  20397. return (sign ? (forceSign ? '+' : '') : '-') + output;
  20398. }
  20399. // helper function for _.addTime and _.subtractTime
  20400. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  20401. var milliseconds = duration._milliseconds,
  20402. days = duration._days,
  20403. months = duration._months;
  20404. updateOffset = updateOffset == null ? true : updateOffset;
  20405. if (milliseconds) {
  20406. mom._d.setTime(+mom._d + milliseconds * isAdding);
  20407. }
  20408. if (days) {
  20409. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  20410. }
  20411. if (months) {
  20412. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  20413. }
  20414. if (updateOffset) {
  20415. moment.updateOffset(mom, days || months);
  20416. }
  20417. }
  20418. // check if is an array
  20419. function isArray(input) {
  20420. return Object.prototype.toString.call(input) === '[object Array]';
  20421. }
  20422. function isDate(input) {
  20423. return Object.prototype.toString.call(input) === '[object Date]' ||
  20424. input instanceof Date;
  20425. }
  20426. // compare two arrays, return the number of differences
  20427. function compareArrays(array1, array2, dontConvert) {
  20428. var len = Math.min(array1.length, array2.length),
  20429. lengthDiff = Math.abs(array1.length - array2.length),
  20430. diffs = 0,
  20431. i;
  20432. for (i = 0; i < len; i++) {
  20433. if ((dontConvert && array1[i] !== array2[i]) ||
  20434. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  20435. diffs++;
  20436. }
  20437. }
  20438. return diffs + lengthDiff;
  20439. }
  20440. function normalizeUnits(units) {
  20441. if (units) {
  20442. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  20443. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  20444. }
  20445. return units;
  20446. }
  20447. function normalizeObjectUnits(inputObject) {
  20448. var normalizedInput = {},
  20449. normalizedProp,
  20450. prop;
  20451. for (prop in inputObject) {
  20452. if (inputObject.hasOwnProperty(prop)) {
  20453. normalizedProp = normalizeUnits(prop);
  20454. if (normalizedProp) {
  20455. normalizedInput[normalizedProp] = inputObject[prop];
  20456. }
  20457. }
  20458. }
  20459. return normalizedInput;
  20460. }
  20461. function makeList(field) {
  20462. var count, setter;
  20463. if (field.indexOf('week') === 0) {
  20464. count = 7;
  20465. setter = 'day';
  20466. }
  20467. else if (field.indexOf('month') === 0) {
  20468. count = 12;
  20469. setter = 'month';
  20470. }
  20471. else {
  20472. return;
  20473. }
  20474. moment[field] = function (format, index) {
  20475. var i, getter,
  20476. method = moment.fn._lang[field],
  20477. results = [];
  20478. if (typeof format === 'number') {
  20479. index = format;
  20480. format = undefined;
  20481. }
  20482. getter = function (i) {
  20483. var m = moment().utc().set(setter, i);
  20484. return method.call(moment.fn._lang, m, format || '');
  20485. };
  20486. if (index != null) {
  20487. return getter(index);
  20488. }
  20489. else {
  20490. for (i = 0; i < count; i++) {
  20491. results.push(getter(i));
  20492. }
  20493. return results;
  20494. }
  20495. };
  20496. }
  20497. function toInt(argumentForCoercion) {
  20498. var coercedNumber = +argumentForCoercion,
  20499. value = 0;
  20500. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  20501. if (coercedNumber >= 0) {
  20502. value = Math.floor(coercedNumber);
  20503. } else {
  20504. value = Math.ceil(coercedNumber);
  20505. }
  20506. }
  20507. return value;
  20508. }
  20509. function daysInMonth(year, month) {
  20510. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  20511. }
  20512. function weeksInYear(year, dow, doy) {
  20513. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  20514. }
  20515. function daysInYear(year) {
  20516. return isLeapYear(year) ? 366 : 365;
  20517. }
  20518. function isLeapYear(year) {
  20519. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  20520. }
  20521. function checkOverflow(m) {
  20522. var overflow;
  20523. if (m._a && m._pf.overflow === -2) {
  20524. overflow =
  20525. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  20526. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  20527. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  20528. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  20529. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  20530. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  20531. -1;
  20532. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  20533. overflow = DATE;
  20534. }
  20535. m._pf.overflow = overflow;
  20536. }
  20537. }
  20538. function isValid(m) {
  20539. if (m._isValid == null) {
  20540. m._isValid = !isNaN(m._d.getTime()) &&
  20541. m._pf.overflow < 0 &&
  20542. !m._pf.empty &&
  20543. !m._pf.invalidMonth &&
  20544. !m._pf.nullInput &&
  20545. !m._pf.invalidFormat &&
  20546. !m._pf.userInvalidated;
  20547. if (m._strict) {
  20548. m._isValid = m._isValid &&
  20549. m._pf.charsLeftOver === 0 &&
  20550. m._pf.unusedTokens.length === 0;
  20551. }
  20552. }
  20553. return m._isValid;
  20554. }
  20555. function normalizeLanguage(key) {
  20556. return key ? key.toLowerCase().replace('_', '-') : key;
  20557. }
  20558. // Return a moment from input, that is local/utc/zone equivalent to model.
  20559. function makeAs(input, model) {
  20560. return model._isUTC ? moment(input).zone(model._offset || 0) :
  20561. moment(input).local();
  20562. }
  20563. /************************************
  20564. Languages
  20565. ************************************/
  20566. extend(Language.prototype, {
  20567. set : function (config) {
  20568. var prop, i;
  20569. for (i in config) {
  20570. prop = config[i];
  20571. if (typeof prop === 'function') {
  20572. this[i] = prop;
  20573. } else {
  20574. this['_' + i] = prop;
  20575. }
  20576. }
  20577. },
  20578. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  20579. months : function (m) {
  20580. return this._months[m.month()];
  20581. },
  20582. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  20583. monthsShort : function (m) {
  20584. return this._monthsShort[m.month()];
  20585. },
  20586. monthsParse : function (monthName) {
  20587. var i, mom, regex;
  20588. if (!this._monthsParse) {
  20589. this._monthsParse = [];
  20590. }
  20591. for (i = 0; i < 12; i++) {
  20592. // make the regex if we don't have it already
  20593. if (!this._monthsParse[i]) {
  20594. mom = moment.utc([2000, i]);
  20595. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  20596. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  20597. }
  20598. // test the regex
  20599. if (this._monthsParse[i].test(monthName)) {
  20600. return i;
  20601. }
  20602. }
  20603. },
  20604. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  20605. weekdays : function (m) {
  20606. return this._weekdays[m.day()];
  20607. },
  20608. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  20609. weekdaysShort : function (m) {
  20610. return this._weekdaysShort[m.day()];
  20611. },
  20612. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  20613. weekdaysMin : function (m) {
  20614. return this._weekdaysMin[m.day()];
  20615. },
  20616. weekdaysParse : function (weekdayName) {
  20617. var i, mom, regex;
  20618. if (!this._weekdaysParse) {
  20619. this._weekdaysParse = [];
  20620. }
  20621. for (i = 0; i < 7; i++) {
  20622. // make the regex if we don't have it already
  20623. if (!this._weekdaysParse[i]) {
  20624. mom = moment([2000, 1]).day(i);
  20625. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  20626. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  20627. }
  20628. // test the regex
  20629. if (this._weekdaysParse[i].test(weekdayName)) {
  20630. return i;
  20631. }
  20632. }
  20633. },
  20634. _longDateFormat : {
  20635. LT : "h:mm A",
  20636. L : "MM/DD/YYYY",
  20637. LL : "MMMM D YYYY",
  20638. LLL : "MMMM D YYYY LT",
  20639. LLLL : "dddd, MMMM D YYYY LT"
  20640. },
  20641. longDateFormat : function (key) {
  20642. var output = this._longDateFormat[key];
  20643. if (!output && this._longDateFormat[key.toUpperCase()]) {
  20644. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  20645. return val.slice(1);
  20646. });
  20647. this._longDateFormat[key] = output;
  20648. }
  20649. return output;
  20650. },
  20651. isPM : function (input) {
  20652. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  20653. // Using charAt should be more compatible.
  20654. return ((input + '').toLowerCase().charAt(0) === 'p');
  20655. },
  20656. _meridiemParse : /[ap]\.?m?\.?/i,
  20657. meridiem : function (hours, minutes, isLower) {
  20658. if (hours > 11) {
  20659. return isLower ? 'pm' : 'PM';
  20660. } else {
  20661. return isLower ? 'am' : 'AM';
  20662. }
  20663. },
  20664. _calendar : {
  20665. sameDay : '[Today at] LT',
  20666. nextDay : '[Tomorrow at] LT',
  20667. nextWeek : 'dddd [at] LT',
  20668. lastDay : '[Yesterday at] LT',
  20669. lastWeek : '[Last] dddd [at] LT',
  20670. sameElse : 'L'
  20671. },
  20672. calendar : function (key, mom) {
  20673. var output = this._calendar[key];
  20674. return typeof output === 'function' ? output.apply(mom) : output;
  20675. },
  20676. _relativeTime : {
  20677. future : "in %s",
  20678. past : "%s ago",
  20679. s : "a few seconds",
  20680. m : "a minute",
  20681. mm : "%d minutes",
  20682. h : "an hour",
  20683. hh : "%d hours",
  20684. d : "a day",
  20685. dd : "%d days",
  20686. M : "a month",
  20687. MM : "%d months",
  20688. y : "a year",
  20689. yy : "%d years"
  20690. },
  20691. relativeTime : function (number, withoutSuffix, string, isFuture) {
  20692. var output = this._relativeTime[string];
  20693. return (typeof output === 'function') ?
  20694. output(number, withoutSuffix, string, isFuture) :
  20695. output.replace(/%d/i, number);
  20696. },
  20697. pastFuture : function (diff, output) {
  20698. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  20699. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  20700. },
  20701. ordinal : function (number) {
  20702. return this._ordinal.replace("%d", number);
  20703. },
  20704. _ordinal : "%d",
  20705. preparse : function (string) {
  20706. return string;
  20707. },
  20708. postformat : function (string) {
  20709. return string;
  20710. },
  20711. week : function (mom) {
  20712. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  20713. },
  20714. _week : {
  20715. dow : 0, // Sunday is the first day of the week.
  20716. doy : 6 // The week that contains Jan 1st is the first week of the year.
  20717. },
  20718. _invalidDate: 'Invalid date',
  20719. invalidDate: function () {
  20720. return this._invalidDate;
  20721. }
  20722. });
  20723. // Loads a language definition into the `languages` cache. The function
  20724. // takes a key and optionally values. If not in the browser and no values
  20725. // are provided, it will load the language file module. As a convenience,
  20726. // this function also returns the language values.
  20727. function loadLang(key, values) {
  20728. values.abbr = key;
  20729. if (!languages[key]) {
  20730. languages[key] = new Language();
  20731. }
  20732. languages[key].set(values);
  20733. return languages[key];
  20734. }
  20735. // Remove a language from the `languages` cache. Mostly useful in tests.
  20736. function unloadLang(key) {
  20737. delete languages[key];
  20738. }
  20739. // Determines which language definition to use and returns it.
  20740. //
  20741. // With no parameters, it will return the global language. If you
  20742. // pass in a language key, such as 'en', it will return the
  20743. // definition for 'en', so long as 'en' has already been loaded using
  20744. // moment.lang.
  20745. function getLangDefinition(key) {
  20746. var i = 0, j, lang, next, split,
  20747. get = function (k) {
  20748. if (!languages[k] && hasModule) {
  20749. try {
  20750. require('./lang/' + k);
  20751. } catch (e) { }
  20752. }
  20753. return languages[k];
  20754. };
  20755. if (!key) {
  20756. return moment.fn._lang;
  20757. }
  20758. if (!isArray(key)) {
  20759. //short-circuit everything else
  20760. lang = get(key);
  20761. if (lang) {
  20762. return lang;
  20763. }
  20764. key = [key];
  20765. }
  20766. //pick the language from the array
  20767. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  20768. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  20769. while (i < key.length) {
  20770. split = normalizeLanguage(key[i]).split('-');
  20771. j = split.length;
  20772. next = normalizeLanguage(key[i + 1]);
  20773. next = next ? next.split('-') : null;
  20774. while (j > 0) {
  20775. lang = get(split.slice(0, j).join('-'));
  20776. if (lang) {
  20777. return lang;
  20778. }
  20779. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  20780. //the next array item is better than a shallower substring of this one
  20781. break;
  20782. }
  20783. j--;
  20784. }
  20785. i++;
  20786. }
  20787. return moment.fn._lang;
  20788. }
  20789. /************************************
  20790. Formatting
  20791. ************************************/
  20792. function removeFormattingTokens(input) {
  20793. if (input.match(/\[[\s\S]/)) {
  20794. return input.replace(/^\[|\]$/g, "");
  20795. }
  20796. return input.replace(/\\/g, "");
  20797. }
  20798. function makeFormatFunction(format) {
  20799. var array = format.match(formattingTokens), i, length;
  20800. for (i = 0, length = array.length; i < length; i++) {
  20801. if (formatTokenFunctions[array[i]]) {
  20802. array[i] = formatTokenFunctions[array[i]];
  20803. } else {
  20804. array[i] = removeFormattingTokens(array[i]);
  20805. }
  20806. }
  20807. return function (mom) {
  20808. var output = "";
  20809. for (i = 0; i < length; i++) {
  20810. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  20811. }
  20812. return output;
  20813. };
  20814. }
  20815. // format date using native date object
  20816. function formatMoment(m, format) {
  20817. if (!m.isValid()) {
  20818. return m.lang().invalidDate();
  20819. }
  20820. format = expandFormat(format, m.lang());
  20821. if (!formatFunctions[format]) {
  20822. formatFunctions[format] = makeFormatFunction(format);
  20823. }
  20824. return formatFunctions[format](m);
  20825. }
  20826. function expandFormat(format, lang) {
  20827. var i = 5;
  20828. function replaceLongDateFormatTokens(input) {
  20829. return lang.longDateFormat(input) || input;
  20830. }
  20831. localFormattingTokens.lastIndex = 0;
  20832. while (i >= 0 && localFormattingTokens.test(format)) {
  20833. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  20834. localFormattingTokens.lastIndex = 0;
  20835. i -= 1;
  20836. }
  20837. return format;
  20838. }
  20839. /************************************
  20840. Parsing
  20841. ************************************/
  20842. // get the regex to find the next token
  20843. function getParseRegexForToken(token, config) {
  20844. var a, strict = config._strict;
  20845. switch (token) {
  20846. case 'Q':
  20847. return parseTokenOneDigit;
  20848. case 'DDDD':
  20849. return parseTokenThreeDigits;
  20850. case 'YYYY':
  20851. case 'GGGG':
  20852. case 'gggg':
  20853. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  20854. case 'Y':
  20855. case 'G':
  20856. case 'g':
  20857. return parseTokenSignedNumber;
  20858. case 'YYYYYY':
  20859. case 'YYYYY':
  20860. case 'GGGGG':
  20861. case 'ggggg':
  20862. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  20863. case 'S':
  20864. if (strict) { return parseTokenOneDigit; }
  20865. /* falls through */
  20866. case 'SS':
  20867. if (strict) { return parseTokenTwoDigits; }
  20868. /* falls through */
  20869. case 'SSS':
  20870. if (strict) { return parseTokenThreeDigits; }
  20871. /* falls through */
  20872. case 'DDD':
  20873. return parseTokenOneToThreeDigits;
  20874. case 'MMM':
  20875. case 'MMMM':
  20876. case 'dd':
  20877. case 'ddd':
  20878. case 'dddd':
  20879. return parseTokenWord;
  20880. case 'a':
  20881. case 'A':
  20882. return getLangDefinition(config._l)._meridiemParse;
  20883. case 'X':
  20884. return parseTokenTimestampMs;
  20885. case 'Z':
  20886. case 'ZZ':
  20887. return parseTokenTimezone;
  20888. case 'T':
  20889. return parseTokenT;
  20890. case 'SSSS':
  20891. return parseTokenDigits;
  20892. case 'MM':
  20893. case 'DD':
  20894. case 'YY':
  20895. case 'GG':
  20896. case 'gg':
  20897. case 'HH':
  20898. case 'hh':
  20899. case 'mm':
  20900. case 'ss':
  20901. case 'ww':
  20902. case 'WW':
  20903. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  20904. case 'M':
  20905. case 'D':
  20906. case 'd':
  20907. case 'H':
  20908. case 'h':
  20909. case 'm':
  20910. case 's':
  20911. case 'w':
  20912. case 'W':
  20913. case 'e':
  20914. case 'E':
  20915. return parseTokenOneOrTwoDigits;
  20916. case 'Do':
  20917. return parseTokenOrdinal;
  20918. default :
  20919. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  20920. return a;
  20921. }
  20922. }
  20923. function timezoneMinutesFromString(string) {
  20924. string = string || "";
  20925. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  20926. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  20927. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  20928. minutes = +(parts[1] * 60) + toInt(parts[2]);
  20929. return parts[0] === '+' ? -minutes : minutes;
  20930. }
  20931. // function to convert string input to date
  20932. function addTimeToArrayFromToken(token, input, config) {
  20933. var a, datePartArray = config._a;
  20934. switch (token) {
  20935. // QUARTER
  20936. case 'Q':
  20937. if (input != null) {
  20938. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  20939. }
  20940. break;
  20941. // MONTH
  20942. case 'M' : // fall through to MM
  20943. case 'MM' :
  20944. if (input != null) {
  20945. datePartArray[MONTH] = toInt(input) - 1;
  20946. }
  20947. break;
  20948. case 'MMM' : // fall through to MMMM
  20949. case 'MMMM' :
  20950. a = getLangDefinition(config._l).monthsParse(input);
  20951. // if we didn't find a month name, mark the date as invalid.
  20952. if (a != null) {
  20953. datePartArray[MONTH] = a;
  20954. } else {
  20955. config._pf.invalidMonth = input;
  20956. }
  20957. break;
  20958. // DAY OF MONTH
  20959. case 'D' : // fall through to DD
  20960. case 'DD' :
  20961. if (input != null) {
  20962. datePartArray[DATE] = toInt(input);
  20963. }
  20964. break;
  20965. case 'Do' :
  20966. if (input != null) {
  20967. datePartArray[DATE] = toInt(parseInt(input, 10));
  20968. }
  20969. break;
  20970. // DAY OF YEAR
  20971. case 'DDD' : // fall through to DDDD
  20972. case 'DDDD' :
  20973. if (input != null) {
  20974. config._dayOfYear = toInt(input);
  20975. }
  20976. break;
  20977. // YEAR
  20978. case 'YY' :
  20979. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  20980. break;
  20981. case 'YYYY' :
  20982. case 'YYYYY' :
  20983. case 'YYYYYY' :
  20984. datePartArray[YEAR] = toInt(input);
  20985. break;
  20986. // AM / PM
  20987. case 'a' : // fall through to A
  20988. case 'A' :
  20989. config._isPm = getLangDefinition(config._l).isPM(input);
  20990. break;
  20991. // 24 HOUR
  20992. case 'H' : // fall through to hh
  20993. case 'HH' : // fall through to hh
  20994. case 'h' : // fall through to hh
  20995. case 'hh' :
  20996. datePartArray[HOUR] = toInt(input);
  20997. break;
  20998. // MINUTE
  20999. case 'm' : // fall through to mm
  21000. case 'mm' :
  21001. datePartArray[MINUTE] = toInt(input);
  21002. break;
  21003. // SECOND
  21004. case 's' : // fall through to ss
  21005. case 'ss' :
  21006. datePartArray[SECOND] = toInt(input);
  21007. break;
  21008. // MILLISECOND
  21009. case 'S' :
  21010. case 'SS' :
  21011. case 'SSS' :
  21012. case 'SSSS' :
  21013. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  21014. break;
  21015. // UNIX TIMESTAMP WITH MS
  21016. case 'X':
  21017. config._d = new Date(parseFloat(input) * 1000);
  21018. break;
  21019. // TIMEZONE
  21020. case 'Z' : // fall through to ZZ
  21021. case 'ZZ' :
  21022. config._useUTC = true;
  21023. config._tzm = timezoneMinutesFromString(input);
  21024. break;
  21025. case 'w':
  21026. case 'ww':
  21027. case 'W':
  21028. case 'WW':
  21029. case 'd':
  21030. case 'dd':
  21031. case 'ddd':
  21032. case 'dddd':
  21033. case 'e':
  21034. case 'E':
  21035. token = token.substr(0, 1);
  21036. /* falls through */
  21037. case 'gg':
  21038. case 'gggg':
  21039. case 'GG':
  21040. case 'GGGG':
  21041. case 'GGGGG':
  21042. token = token.substr(0, 2);
  21043. if (input) {
  21044. config._w = config._w || {};
  21045. config._w[token] = input;
  21046. }
  21047. break;
  21048. }
  21049. }
  21050. // convert an array to a date.
  21051. // the array should mirror the parameters below
  21052. // note: all values past the year are optional and will default to the lowest possible value.
  21053. // [year, month, day , hour, minute, second, millisecond]
  21054. function dateFromConfig(config) {
  21055. var i, date, input = [], currentDate,
  21056. yearToUse, fixYear, w, temp, lang, weekday, week;
  21057. if (config._d) {
  21058. return;
  21059. }
  21060. currentDate = currentDateArray(config);
  21061. //compute day of the year from weeks and weekdays
  21062. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  21063. fixYear = function (val) {
  21064. var intVal = parseInt(val, 10);
  21065. return val ?
  21066. (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
  21067. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  21068. };
  21069. w = config._w;
  21070. if (w.GG != null || w.W != null || w.E != null) {
  21071. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  21072. }
  21073. else {
  21074. lang = getLangDefinition(config._l);
  21075. weekday = w.d != null ? parseWeekday(w.d, lang) :
  21076. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  21077. week = parseInt(w.w, 10) || 1;
  21078. //if we're parsing 'd', then the low day numbers may be next week
  21079. if (w.d != null && weekday < lang._week.dow) {
  21080. week++;
  21081. }
  21082. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  21083. }
  21084. config._a[YEAR] = temp.year;
  21085. config._dayOfYear = temp.dayOfYear;
  21086. }
  21087. //if the day of the year is set, figure out what it is
  21088. if (config._dayOfYear) {
  21089. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  21090. if (config._dayOfYear > daysInYear(yearToUse)) {
  21091. config._pf._overflowDayOfYear = true;
  21092. }
  21093. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  21094. config._a[MONTH] = date.getUTCMonth();
  21095. config._a[DATE] = date.getUTCDate();
  21096. }
  21097. // Default to current date.
  21098. // * if no year, month, day of month are given, default to today
  21099. // * if day of month is given, default month and year
  21100. // * if month is given, default only year
  21101. // * if year is given, don't default anything
  21102. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  21103. config._a[i] = input[i] = currentDate[i];
  21104. }
  21105. // Zero out whatever was not defaulted, including time
  21106. for (; i < 7; i++) {
  21107. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  21108. }
  21109. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  21110. input[HOUR] += toInt((config._tzm || 0) / 60);
  21111. input[MINUTE] += toInt((config._tzm || 0) % 60);
  21112. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  21113. }
  21114. function dateFromObject(config) {
  21115. var normalizedInput;
  21116. if (config._d) {
  21117. return;
  21118. }
  21119. normalizedInput = normalizeObjectUnits(config._i);
  21120. config._a = [
  21121. normalizedInput.year,
  21122. normalizedInput.month,
  21123. normalizedInput.day,
  21124. normalizedInput.hour,
  21125. normalizedInput.minute,
  21126. normalizedInput.second,
  21127. normalizedInput.millisecond
  21128. ];
  21129. dateFromConfig(config);
  21130. }
  21131. function currentDateArray(config) {
  21132. var now = new Date();
  21133. if (config._useUTC) {
  21134. return [
  21135. now.getUTCFullYear(),
  21136. now.getUTCMonth(),
  21137. now.getUTCDate()
  21138. ];
  21139. } else {
  21140. return [now.getFullYear(), now.getMonth(), now.getDate()];
  21141. }
  21142. }
  21143. // date from string and format string
  21144. function makeDateFromStringAndFormat(config) {
  21145. config._a = [];
  21146. config._pf.empty = true;
  21147. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  21148. var lang = getLangDefinition(config._l),
  21149. string = '' + config._i,
  21150. i, parsedInput, tokens, token, skipped,
  21151. stringLength = string.length,
  21152. totalParsedInputLength = 0;
  21153. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  21154. for (i = 0; i < tokens.length; i++) {
  21155. token = tokens[i];
  21156. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  21157. if (parsedInput) {
  21158. skipped = string.substr(0, string.indexOf(parsedInput));
  21159. if (skipped.length > 0) {
  21160. config._pf.unusedInput.push(skipped);
  21161. }
  21162. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  21163. totalParsedInputLength += parsedInput.length;
  21164. }
  21165. // don't parse if it's not a known token
  21166. if (formatTokenFunctions[token]) {
  21167. if (parsedInput) {
  21168. config._pf.empty = false;
  21169. }
  21170. else {
  21171. config._pf.unusedTokens.push(token);
  21172. }
  21173. addTimeToArrayFromToken(token, parsedInput, config);
  21174. }
  21175. else if (config._strict && !parsedInput) {
  21176. config._pf.unusedTokens.push(token);
  21177. }
  21178. }
  21179. // add remaining unparsed input length to the string
  21180. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  21181. if (string.length > 0) {
  21182. config._pf.unusedInput.push(string);
  21183. }
  21184. // handle am pm
  21185. if (config._isPm && config._a[HOUR] < 12) {
  21186. config._a[HOUR] += 12;
  21187. }
  21188. // if is 12 am, change hours to 0
  21189. if (config._isPm === false && config._a[HOUR] === 12) {
  21190. config._a[HOUR] = 0;
  21191. }
  21192. dateFromConfig(config);
  21193. checkOverflow(config);
  21194. }
  21195. function unescapeFormat(s) {
  21196. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  21197. return p1 || p2 || p3 || p4;
  21198. });
  21199. }
  21200. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  21201. function regexpEscape(s) {
  21202. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  21203. }
  21204. // date from string and array of format strings
  21205. function makeDateFromStringAndArray(config) {
  21206. var tempConfig,
  21207. bestMoment,
  21208. scoreToBeat,
  21209. i,
  21210. currentScore;
  21211. if (config._f.length === 0) {
  21212. config._pf.invalidFormat = true;
  21213. config._d = new Date(NaN);
  21214. return;
  21215. }
  21216. for (i = 0; i < config._f.length; i++) {
  21217. currentScore = 0;
  21218. tempConfig = extend({}, config);
  21219. tempConfig._pf = defaultParsingFlags();
  21220. tempConfig._f = config._f[i];
  21221. makeDateFromStringAndFormat(tempConfig);
  21222. if (!isValid(tempConfig)) {
  21223. continue;
  21224. }
  21225. // if there is any input that was not parsed add a penalty for that format
  21226. currentScore += tempConfig._pf.charsLeftOver;
  21227. //or tokens
  21228. currentScore += tempConfig._pf.unusedTokens.length * 10;
  21229. tempConfig._pf.score = currentScore;
  21230. if (scoreToBeat == null || currentScore < scoreToBeat) {
  21231. scoreToBeat = currentScore;
  21232. bestMoment = tempConfig;
  21233. }
  21234. }
  21235. extend(config, bestMoment || tempConfig);
  21236. }
  21237. // date from iso format
  21238. function makeDateFromString(config) {
  21239. var i, l,
  21240. string = config._i,
  21241. match = isoRegex.exec(string);
  21242. if (match) {
  21243. config._pf.iso = true;
  21244. for (i = 0, l = isoDates.length; i < l; i++) {
  21245. if (isoDates[i][1].exec(string)) {
  21246. // match[5] should be "T" or undefined
  21247. config._f = isoDates[i][0] + (match[6] || " ");
  21248. break;
  21249. }
  21250. }
  21251. for (i = 0, l = isoTimes.length; i < l; i++) {
  21252. if (isoTimes[i][1].exec(string)) {
  21253. config._f += isoTimes[i][0];
  21254. break;
  21255. }
  21256. }
  21257. if (string.match(parseTokenTimezone)) {
  21258. config._f += "Z";
  21259. }
  21260. makeDateFromStringAndFormat(config);
  21261. }
  21262. else {
  21263. moment.createFromInputFallback(config);
  21264. }
  21265. }
  21266. function makeDateFromInput(config) {
  21267. var input = config._i,
  21268. matched = aspNetJsonRegex.exec(input);
  21269. if (input === undefined) {
  21270. config._d = new Date();
  21271. } else if (matched) {
  21272. config._d = new Date(+matched[1]);
  21273. } else if (typeof input === 'string') {
  21274. makeDateFromString(config);
  21275. } else if (isArray(input)) {
  21276. config._a = input.slice(0);
  21277. dateFromConfig(config);
  21278. } else if (isDate(input)) {
  21279. config._d = new Date(+input);
  21280. } else if (typeof(input) === 'object') {
  21281. dateFromObject(config);
  21282. } else if (typeof(input) === 'number') {
  21283. // from milliseconds
  21284. config._d = new Date(input);
  21285. } else {
  21286. moment.createFromInputFallback(config);
  21287. }
  21288. }
  21289. function makeDate(y, m, d, h, M, s, ms) {
  21290. //can't just apply() to create a date:
  21291. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  21292. var date = new Date(y, m, d, h, M, s, ms);
  21293. //the date constructor doesn't accept years < 1970
  21294. if (y < 1970) {
  21295. date.setFullYear(y);
  21296. }
  21297. return date;
  21298. }
  21299. function makeUTCDate(y) {
  21300. var date = new Date(Date.UTC.apply(null, arguments));
  21301. if (y < 1970) {
  21302. date.setUTCFullYear(y);
  21303. }
  21304. return date;
  21305. }
  21306. function parseWeekday(input, language) {
  21307. if (typeof input === 'string') {
  21308. if (!isNaN(input)) {
  21309. input = parseInt(input, 10);
  21310. }
  21311. else {
  21312. input = language.weekdaysParse(input);
  21313. if (typeof input !== 'number') {
  21314. return null;
  21315. }
  21316. }
  21317. }
  21318. return input;
  21319. }
  21320. /************************************
  21321. Relative Time
  21322. ************************************/
  21323. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  21324. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  21325. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  21326. }
  21327. function relativeTime(milliseconds, withoutSuffix, lang) {
  21328. var seconds = round(Math.abs(milliseconds) / 1000),
  21329. minutes = round(seconds / 60),
  21330. hours = round(minutes / 60),
  21331. days = round(hours / 24),
  21332. years = round(days / 365),
  21333. args = seconds < 45 && ['s', seconds] ||
  21334. minutes === 1 && ['m'] ||
  21335. minutes < 45 && ['mm', minutes] ||
  21336. hours === 1 && ['h'] ||
  21337. hours < 22 && ['hh', hours] ||
  21338. days === 1 && ['d'] ||
  21339. days <= 25 && ['dd', days] ||
  21340. days <= 45 && ['M'] ||
  21341. days < 345 && ['MM', round(days / 30)] ||
  21342. years === 1 && ['y'] || ['yy', years];
  21343. args[2] = withoutSuffix;
  21344. args[3] = milliseconds > 0;
  21345. args[4] = lang;
  21346. return substituteTimeAgo.apply({}, args);
  21347. }
  21348. /************************************
  21349. Week of Year
  21350. ************************************/
  21351. // firstDayOfWeek 0 = sun, 6 = sat
  21352. // the day of the week that starts the week
  21353. // (usually sunday or monday)
  21354. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  21355. // the first week is the week that contains the first
  21356. // of this day of the week
  21357. // (eg. ISO weeks use thursday (4))
  21358. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  21359. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  21360. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  21361. adjustedMoment;
  21362. if (daysToDayOfWeek > end) {
  21363. daysToDayOfWeek -= 7;
  21364. }
  21365. if (daysToDayOfWeek < end - 7) {
  21366. daysToDayOfWeek += 7;
  21367. }
  21368. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  21369. return {
  21370. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  21371. year: adjustedMoment.year()
  21372. };
  21373. }
  21374. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  21375. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  21376. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  21377. weekday = weekday != null ? weekday : firstDayOfWeek;
  21378. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  21379. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  21380. return {
  21381. year: dayOfYear > 0 ? year : year - 1,
  21382. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  21383. };
  21384. }
  21385. /************************************
  21386. Top Level Functions
  21387. ************************************/
  21388. function makeMoment(config) {
  21389. var input = config._i,
  21390. format = config._f;
  21391. if (input === null || (format === undefined && input === '')) {
  21392. return moment.invalid({nullInput: true});
  21393. }
  21394. if (typeof input === 'string') {
  21395. config._i = input = getLangDefinition().preparse(input);
  21396. }
  21397. if (moment.isMoment(input)) {
  21398. config = cloneMoment(input);
  21399. config._d = new Date(+input._d);
  21400. } else if (format) {
  21401. if (isArray(format)) {
  21402. makeDateFromStringAndArray(config);
  21403. } else {
  21404. makeDateFromStringAndFormat(config);
  21405. }
  21406. } else {
  21407. makeDateFromInput(config);
  21408. }
  21409. return new Moment(config);
  21410. }
  21411. moment = function (input, format, lang, strict) {
  21412. var c;
  21413. if (typeof(lang) === "boolean") {
  21414. strict = lang;
  21415. lang = undefined;
  21416. }
  21417. // object construction must be done this way.
  21418. // https://github.com/moment/moment/issues/1423
  21419. c = {};
  21420. c._isAMomentObject = true;
  21421. c._i = input;
  21422. c._f = format;
  21423. c._l = lang;
  21424. c._strict = strict;
  21425. c._isUTC = false;
  21426. c._pf = defaultParsingFlags();
  21427. return makeMoment(c);
  21428. };
  21429. moment.suppressDeprecationWarnings = false;
  21430. moment.createFromInputFallback = deprecate(
  21431. "moment construction falls back to js Date. This is " +
  21432. "discouraged and will be removed in upcoming major " +
  21433. "release. Please refer to " +
  21434. "https://github.com/moment/moment/issues/1407 for more info.",
  21435. function (config) {
  21436. config._d = new Date(config._i);
  21437. });
  21438. // creating with utc
  21439. moment.utc = function (input, format, lang, strict) {
  21440. var c;
  21441. if (typeof(lang) === "boolean") {
  21442. strict = lang;
  21443. lang = undefined;
  21444. }
  21445. // object construction must be done this way.
  21446. // https://github.com/moment/moment/issues/1423
  21447. c = {};
  21448. c._isAMomentObject = true;
  21449. c._useUTC = true;
  21450. c._isUTC = true;
  21451. c._l = lang;
  21452. c._i = input;
  21453. c._f = format;
  21454. c._strict = strict;
  21455. c._pf = defaultParsingFlags();
  21456. return makeMoment(c).utc();
  21457. };
  21458. // creating with unix timestamp (in seconds)
  21459. moment.unix = function (input) {
  21460. return moment(input * 1000);
  21461. };
  21462. // duration
  21463. moment.duration = function (input, key) {
  21464. var duration = input,
  21465. // matching against regexp is expensive, do it on demand
  21466. match = null,
  21467. sign,
  21468. ret,
  21469. parseIso;
  21470. if (moment.isDuration(input)) {
  21471. duration = {
  21472. ms: input._milliseconds,
  21473. d: input._days,
  21474. M: input._months
  21475. };
  21476. } else if (typeof input === 'number') {
  21477. duration = {};
  21478. if (key) {
  21479. duration[key] = input;
  21480. } else {
  21481. duration.milliseconds = input;
  21482. }
  21483. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  21484. sign = (match[1] === "-") ? -1 : 1;
  21485. duration = {
  21486. y: 0,
  21487. d: toInt(match[DATE]) * sign,
  21488. h: toInt(match[HOUR]) * sign,
  21489. m: toInt(match[MINUTE]) * sign,
  21490. s: toInt(match[SECOND]) * sign,
  21491. ms: toInt(match[MILLISECOND]) * sign
  21492. };
  21493. } else if (!!(match = isoDurationRegex.exec(input))) {
  21494. sign = (match[1] === "-") ? -1 : 1;
  21495. parseIso = function (inp) {
  21496. // We'd normally use ~~inp for this, but unfortunately it also
  21497. // converts floats to ints.
  21498. // inp may be undefined, so careful calling replace on it.
  21499. var res = inp && parseFloat(inp.replace(',', '.'));
  21500. // apply sign while we're at it
  21501. return (isNaN(res) ? 0 : res) * sign;
  21502. };
  21503. duration = {
  21504. y: parseIso(match[2]),
  21505. M: parseIso(match[3]),
  21506. d: parseIso(match[4]),
  21507. h: parseIso(match[5]),
  21508. m: parseIso(match[6]),
  21509. s: parseIso(match[7]),
  21510. w: parseIso(match[8])
  21511. };
  21512. }
  21513. ret = new Duration(duration);
  21514. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  21515. ret._lang = input._lang;
  21516. }
  21517. return ret;
  21518. };
  21519. // version number
  21520. moment.version = VERSION;
  21521. // default format
  21522. moment.defaultFormat = isoFormat;
  21523. // Plugins that add properties should also add the key here (null value),
  21524. // so we can properly clone ourselves.
  21525. moment.momentProperties = momentProperties;
  21526. // This function will be called whenever a moment is mutated.
  21527. // It is intended to keep the offset in sync with the timezone.
  21528. moment.updateOffset = function () {};
  21529. // This function will load languages and then set the global language. If
  21530. // no arguments are passed in, it will simply return the current global
  21531. // language key.
  21532. moment.lang = function (key, values) {
  21533. var r;
  21534. if (!key) {
  21535. return moment.fn._lang._abbr;
  21536. }
  21537. if (values) {
  21538. loadLang(normalizeLanguage(key), values);
  21539. } else if (values === null) {
  21540. unloadLang(key);
  21541. key = 'en';
  21542. } else if (!languages[key]) {
  21543. getLangDefinition(key);
  21544. }
  21545. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  21546. return r._abbr;
  21547. };
  21548. // returns language data
  21549. moment.langData = function (key) {
  21550. if (key && key._lang && key._lang._abbr) {
  21551. key = key._lang._abbr;
  21552. }
  21553. return getLangDefinition(key);
  21554. };
  21555. // compare moment object
  21556. moment.isMoment = function (obj) {
  21557. return obj instanceof Moment ||
  21558. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  21559. };
  21560. // for typechecking Duration objects
  21561. moment.isDuration = function (obj) {
  21562. return obj instanceof Duration;
  21563. };
  21564. for (i = lists.length - 1; i >= 0; --i) {
  21565. makeList(lists[i]);
  21566. }
  21567. moment.normalizeUnits = function (units) {
  21568. return normalizeUnits(units);
  21569. };
  21570. moment.invalid = function (flags) {
  21571. var m = moment.utc(NaN);
  21572. if (flags != null) {
  21573. extend(m._pf, flags);
  21574. }
  21575. else {
  21576. m._pf.userInvalidated = true;
  21577. }
  21578. return m;
  21579. };
  21580. moment.parseZone = function () {
  21581. return moment.apply(null, arguments).parseZone();
  21582. };
  21583. moment.parseTwoDigitYear = function (input) {
  21584. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  21585. };
  21586. /************************************
  21587. Moment Prototype
  21588. ************************************/
  21589. extend(moment.fn = Moment.prototype, {
  21590. clone : function () {
  21591. return moment(this);
  21592. },
  21593. valueOf : function () {
  21594. return +this._d + ((this._offset || 0) * 60000);
  21595. },
  21596. unix : function () {
  21597. return Math.floor(+this / 1000);
  21598. },
  21599. toString : function () {
  21600. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  21601. },
  21602. toDate : function () {
  21603. return this._offset ? new Date(+this) : this._d;
  21604. },
  21605. toISOString : function () {
  21606. var m = moment(this).utc();
  21607. if (0 < m.year() && m.year() <= 9999) {
  21608. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  21609. } else {
  21610. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  21611. }
  21612. },
  21613. toArray : function () {
  21614. var m = this;
  21615. return [
  21616. m.year(),
  21617. m.month(),
  21618. m.date(),
  21619. m.hours(),
  21620. m.minutes(),
  21621. m.seconds(),
  21622. m.milliseconds()
  21623. ];
  21624. },
  21625. isValid : function () {
  21626. return isValid(this);
  21627. },
  21628. isDSTShifted : function () {
  21629. if (this._a) {
  21630. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  21631. }
  21632. return false;
  21633. },
  21634. parsingFlags : function () {
  21635. return extend({}, this._pf);
  21636. },
  21637. invalidAt: function () {
  21638. return this._pf.overflow;
  21639. },
  21640. utc : function () {
  21641. return this.zone(0);
  21642. },
  21643. local : function () {
  21644. this.zone(0);
  21645. this._isUTC = false;
  21646. return this;
  21647. },
  21648. format : function (inputString) {
  21649. var output = formatMoment(this, inputString || moment.defaultFormat);
  21650. return this.lang().postformat(output);
  21651. },
  21652. add : function (input, val) {
  21653. var dur;
  21654. // switch args to support add('s', 1) and add(1, 's')
  21655. if (typeof input === 'string') {
  21656. dur = moment.duration(+val, input);
  21657. } else {
  21658. dur = moment.duration(input, val);
  21659. }
  21660. addOrSubtractDurationFromMoment(this, dur, 1);
  21661. return this;
  21662. },
  21663. subtract : function (input, val) {
  21664. var dur;
  21665. // switch args to support subtract('s', 1) and subtract(1, 's')
  21666. if (typeof input === 'string') {
  21667. dur = moment.duration(+val, input);
  21668. } else {
  21669. dur = moment.duration(input, val);
  21670. }
  21671. addOrSubtractDurationFromMoment(this, dur, -1);
  21672. return this;
  21673. },
  21674. diff : function (input, units, asFloat) {
  21675. var that = makeAs(input, this),
  21676. zoneDiff = (this.zone() - that.zone()) * 6e4,
  21677. diff, output;
  21678. units = normalizeUnits(units);
  21679. if (units === 'year' || units === 'month') {
  21680. // average number of days in the months in the given dates
  21681. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  21682. // difference in months
  21683. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  21684. // adjust by taking difference in days, average number of days
  21685. // and dst in the given months.
  21686. output += ((this - moment(this).startOf('month')) -
  21687. (that - moment(that).startOf('month'))) / diff;
  21688. // same as above but with zones, to negate all dst
  21689. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  21690. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  21691. if (units === 'year') {
  21692. output = output / 12;
  21693. }
  21694. } else {
  21695. diff = (this - that);
  21696. output = units === 'second' ? diff / 1e3 : // 1000
  21697. units === 'minute' ? diff / 6e4 : // 1000 * 60
  21698. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  21699. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  21700. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  21701. diff;
  21702. }
  21703. return asFloat ? output : absRound(output);
  21704. },
  21705. from : function (time, withoutSuffix) {
  21706. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  21707. },
  21708. fromNow : function (withoutSuffix) {
  21709. return this.from(moment(), withoutSuffix);
  21710. },
  21711. calendar : function () {
  21712. // We want to compare the start of today, vs this.
  21713. // Getting start-of-today depends on whether we're zone'd or not.
  21714. var sod = makeAs(moment(), this).startOf('day'),
  21715. diff = this.diff(sod, 'days', true),
  21716. format = diff < -6 ? 'sameElse' :
  21717. diff < -1 ? 'lastWeek' :
  21718. diff < 0 ? 'lastDay' :
  21719. diff < 1 ? 'sameDay' :
  21720. diff < 2 ? 'nextDay' :
  21721. diff < 7 ? 'nextWeek' : 'sameElse';
  21722. return this.format(this.lang().calendar(format, this));
  21723. },
  21724. isLeapYear : function () {
  21725. return isLeapYear(this.year());
  21726. },
  21727. isDST : function () {
  21728. return (this.zone() < this.clone().month(0).zone() ||
  21729. this.zone() < this.clone().month(5).zone());
  21730. },
  21731. day : function (input) {
  21732. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  21733. if (input != null) {
  21734. input = parseWeekday(input, this.lang());
  21735. return this.add({ d : input - day });
  21736. } else {
  21737. return day;
  21738. }
  21739. },
  21740. month : makeAccessor('Month', true),
  21741. startOf: function (units) {
  21742. units = normalizeUnits(units);
  21743. // the following switch intentionally omits break keywords
  21744. // to utilize falling through the cases.
  21745. switch (units) {
  21746. case 'year':
  21747. this.month(0);
  21748. /* falls through */
  21749. case 'quarter':
  21750. case 'month':
  21751. this.date(1);
  21752. /* falls through */
  21753. case 'week':
  21754. case 'isoWeek':
  21755. case 'day':
  21756. this.hours(0);
  21757. /* falls through */
  21758. case 'hour':
  21759. this.minutes(0);
  21760. /* falls through */
  21761. case 'minute':
  21762. this.seconds(0);
  21763. /* falls through */
  21764. case 'second':
  21765. this.milliseconds(0);
  21766. /* falls through */
  21767. }
  21768. // weeks are a special case
  21769. if (units === 'week') {
  21770. this.weekday(0);
  21771. } else if (units === 'isoWeek') {
  21772. this.isoWeekday(1);
  21773. }
  21774. // quarters are also special
  21775. if (units === 'quarter') {
  21776. this.month(Math.floor(this.month() / 3) * 3);
  21777. }
  21778. return this;
  21779. },
  21780. endOf: function (units) {
  21781. units = normalizeUnits(units);
  21782. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  21783. },
  21784. isAfter: function (input, units) {
  21785. units = typeof units !== 'undefined' ? units : 'millisecond';
  21786. return +this.clone().startOf(units) > +moment(input).startOf(units);
  21787. },
  21788. isBefore: function (input, units) {
  21789. units = typeof units !== 'undefined' ? units : 'millisecond';
  21790. return +this.clone().startOf(units) < +moment(input).startOf(units);
  21791. },
  21792. isSame: function (input, units) {
  21793. units = units || 'ms';
  21794. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  21795. },
  21796. min: function (other) {
  21797. other = moment.apply(null, arguments);
  21798. return other < this ? this : other;
  21799. },
  21800. max: function (other) {
  21801. other = moment.apply(null, arguments);
  21802. return other > this ? this : other;
  21803. },
  21804. // keepTime = true means only change the timezone, without affecting
  21805. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  21806. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  21807. // adjust the time as needed, to be valid.
  21808. //
  21809. // Keeping the time actually adds/subtracts (one hour)
  21810. // from the actual represented time. That is why we call updateOffset
  21811. // a second time. In case it wants us to change the offset again
  21812. // _changeInProgress == true case, then we have to adjust, because
  21813. // there is no such time in the given timezone.
  21814. zone : function (input, keepTime) {
  21815. var offset = this._offset || 0;
  21816. if (input != null) {
  21817. if (typeof input === "string") {
  21818. input = timezoneMinutesFromString(input);
  21819. }
  21820. if (Math.abs(input) < 16) {
  21821. input = input * 60;
  21822. }
  21823. this._offset = input;
  21824. this._isUTC = true;
  21825. if (offset !== input) {
  21826. if (!keepTime || this._changeInProgress) {
  21827. addOrSubtractDurationFromMoment(this,
  21828. moment.duration(offset - input, 'm'), 1, false);
  21829. } else if (!this._changeInProgress) {
  21830. this._changeInProgress = true;
  21831. moment.updateOffset(this, true);
  21832. this._changeInProgress = null;
  21833. }
  21834. }
  21835. } else {
  21836. return this._isUTC ? offset : this._d.getTimezoneOffset();
  21837. }
  21838. return this;
  21839. },
  21840. zoneAbbr : function () {
  21841. return this._isUTC ? "UTC" : "";
  21842. },
  21843. zoneName : function () {
  21844. return this._isUTC ? "Coordinated Universal Time" : "";
  21845. },
  21846. parseZone : function () {
  21847. if (this._tzm) {
  21848. this.zone(this._tzm);
  21849. } else if (typeof this._i === 'string') {
  21850. this.zone(this._i);
  21851. }
  21852. return this;
  21853. },
  21854. hasAlignedHourOffset : function (input) {
  21855. if (!input) {
  21856. input = 0;
  21857. }
  21858. else {
  21859. input = moment(input).zone();
  21860. }
  21861. return (this.zone() - input) % 60 === 0;
  21862. },
  21863. daysInMonth : function () {
  21864. return daysInMonth(this.year(), this.month());
  21865. },
  21866. dayOfYear : function (input) {
  21867. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  21868. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  21869. },
  21870. quarter : function (input) {
  21871. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  21872. },
  21873. weekYear : function (input) {
  21874. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  21875. return input == null ? year : this.add("y", (input - year));
  21876. },
  21877. isoWeekYear : function (input) {
  21878. var year = weekOfYear(this, 1, 4).year;
  21879. return input == null ? year : this.add("y", (input - year));
  21880. },
  21881. week : function (input) {
  21882. var week = this.lang().week(this);
  21883. return input == null ? week : this.add("d", (input - week) * 7);
  21884. },
  21885. isoWeek : function (input) {
  21886. var week = weekOfYear(this, 1, 4).week;
  21887. return input == null ? week : this.add("d", (input - week) * 7);
  21888. },
  21889. weekday : function (input) {
  21890. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  21891. return input == null ? weekday : this.add("d", input - weekday);
  21892. },
  21893. isoWeekday : function (input) {
  21894. // behaves the same as moment#day except
  21895. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  21896. // as a setter, sunday should belong to the previous week.
  21897. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  21898. },
  21899. isoWeeksInYear : function () {
  21900. return weeksInYear(this.year(), 1, 4);
  21901. },
  21902. weeksInYear : function () {
  21903. var weekInfo = this._lang._week;
  21904. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  21905. },
  21906. get : function (units) {
  21907. units = normalizeUnits(units);
  21908. return this[units]();
  21909. },
  21910. set : function (units, value) {
  21911. units = normalizeUnits(units);
  21912. if (typeof this[units] === 'function') {
  21913. this[units](value);
  21914. }
  21915. return this;
  21916. },
  21917. // If passed a language key, it will set the language for this
  21918. // instance. Otherwise, it will return the language configuration
  21919. // variables for this instance.
  21920. lang : function (key) {
  21921. if (key === undefined) {
  21922. return this._lang;
  21923. } else {
  21924. this._lang = getLangDefinition(key);
  21925. return this;
  21926. }
  21927. }
  21928. });
  21929. function rawMonthSetter(mom, value) {
  21930. var dayOfMonth;
  21931. // TODO: Move this out of here!
  21932. if (typeof value === 'string') {
  21933. value = mom.lang().monthsParse(value);
  21934. // TODO: Another silent failure?
  21935. if (typeof value !== 'number') {
  21936. return mom;
  21937. }
  21938. }
  21939. dayOfMonth = Math.min(mom.date(),
  21940. daysInMonth(mom.year(), value));
  21941. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  21942. return mom;
  21943. }
  21944. function rawGetter(mom, unit) {
  21945. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  21946. }
  21947. function rawSetter(mom, unit, value) {
  21948. if (unit === 'Month') {
  21949. return rawMonthSetter(mom, value);
  21950. } else {
  21951. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  21952. }
  21953. }
  21954. function makeAccessor(unit, keepTime) {
  21955. return function (value) {
  21956. if (value != null) {
  21957. rawSetter(this, unit, value);
  21958. moment.updateOffset(this, keepTime);
  21959. return this;
  21960. } else {
  21961. return rawGetter(this, unit);
  21962. }
  21963. };
  21964. }
  21965. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  21966. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  21967. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  21968. // Setting the hour should keep the time, because the user explicitly
  21969. // specified which hour he wants. So trying to maintain the same hour (in
  21970. // a new timezone) makes sense. Adding/subtracting hours does not follow
  21971. // this rule.
  21972. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  21973. // moment.fn.month is defined separately
  21974. moment.fn.date = makeAccessor('Date', true);
  21975. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  21976. moment.fn.year = makeAccessor('FullYear', true);
  21977. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  21978. // add plural methods
  21979. moment.fn.days = moment.fn.day;
  21980. moment.fn.months = moment.fn.month;
  21981. moment.fn.weeks = moment.fn.week;
  21982. moment.fn.isoWeeks = moment.fn.isoWeek;
  21983. moment.fn.quarters = moment.fn.quarter;
  21984. // add aliased format methods
  21985. moment.fn.toJSON = moment.fn.toISOString;
  21986. /************************************
  21987. Duration Prototype
  21988. ************************************/
  21989. extend(moment.duration.fn = Duration.prototype, {
  21990. _bubble : function () {
  21991. var milliseconds = this._milliseconds,
  21992. days = this._days,
  21993. months = this._months,
  21994. data = this._data,
  21995. seconds, minutes, hours, years;
  21996. // The following code bubbles up values, see the tests for
  21997. // examples of what that means.
  21998. data.milliseconds = milliseconds % 1000;
  21999. seconds = absRound(milliseconds / 1000);
  22000. data.seconds = seconds % 60;
  22001. minutes = absRound(seconds / 60);
  22002. data.minutes = minutes % 60;
  22003. hours = absRound(minutes / 60);
  22004. data.hours = hours % 24;
  22005. days += absRound(hours / 24);
  22006. data.days = days % 30;
  22007. months += absRound(days / 30);
  22008. data.months = months % 12;
  22009. years = absRound(months / 12);
  22010. data.years = years;
  22011. },
  22012. weeks : function () {
  22013. return absRound(this.days() / 7);
  22014. },
  22015. valueOf : function () {
  22016. return this._milliseconds +
  22017. this._days * 864e5 +
  22018. (this._months % 12) * 2592e6 +
  22019. toInt(this._months / 12) * 31536e6;
  22020. },
  22021. humanize : function (withSuffix) {
  22022. var difference = +this,
  22023. output = relativeTime(difference, !withSuffix, this.lang());
  22024. if (withSuffix) {
  22025. output = this.lang().pastFuture(difference, output);
  22026. }
  22027. return this.lang().postformat(output);
  22028. },
  22029. add : function (input, val) {
  22030. // supports only 2.0-style add(1, 's') or add(moment)
  22031. var dur = moment.duration(input, val);
  22032. this._milliseconds += dur._milliseconds;
  22033. this._days += dur._days;
  22034. this._months += dur._months;
  22035. this._bubble();
  22036. return this;
  22037. },
  22038. subtract : function (input, val) {
  22039. var dur = moment.duration(input, val);
  22040. this._milliseconds -= dur._milliseconds;
  22041. this._days -= dur._days;
  22042. this._months -= dur._months;
  22043. this._bubble();
  22044. return this;
  22045. },
  22046. get : function (units) {
  22047. units = normalizeUnits(units);
  22048. return this[units.toLowerCase() + 's']();
  22049. },
  22050. as : function (units) {
  22051. units = normalizeUnits(units);
  22052. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  22053. },
  22054. lang : moment.fn.lang,
  22055. toIsoString : function () {
  22056. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  22057. var years = Math.abs(this.years()),
  22058. months = Math.abs(this.months()),
  22059. days = Math.abs(this.days()),
  22060. hours = Math.abs(this.hours()),
  22061. minutes = Math.abs(this.minutes()),
  22062. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  22063. if (!this.asSeconds()) {
  22064. // this is the same as C#'s (Noda) and python (isodate)...
  22065. // but not other JS (goog.date)
  22066. return 'P0D';
  22067. }
  22068. return (this.asSeconds() < 0 ? '-' : '') +
  22069. 'P' +
  22070. (years ? years + 'Y' : '') +
  22071. (months ? months + 'M' : '') +
  22072. (days ? days + 'D' : '') +
  22073. ((hours || minutes || seconds) ? 'T' : '') +
  22074. (hours ? hours + 'H' : '') +
  22075. (minutes ? minutes + 'M' : '') +
  22076. (seconds ? seconds + 'S' : '');
  22077. }
  22078. });
  22079. function makeDurationGetter(name) {
  22080. moment.duration.fn[name] = function () {
  22081. return this._data[name];
  22082. };
  22083. }
  22084. function makeDurationAsGetter(name, factor) {
  22085. moment.duration.fn['as' + name] = function () {
  22086. return +this / factor;
  22087. };
  22088. }
  22089. for (i in unitMillisecondFactors) {
  22090. if (unitMillisecondFactors.hasOwnProperty(i)) {
  22091. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  22092. makeDurationGetter(i.toLowerCase());
  22093. }
  22094. }
  22095. makeDurationAsGetter('Weeks', 6048e5);
  22096. moment.duration.fn.asMonths = function () {
  22097. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  22098. };
  22099. /************************************
  22100. Default Lang
  22101. ************************************/
  22102. // Set default language, other languages will inherit from English.
  22103. moment.lang('en', {
  22104. ordinal : function (number) {
  22105. var b = number % 10,
  22106. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  22107. (b === 1) ? 'st' :
  22108. (b === 2) ? 'nd' :
  22109. (b === 3) ? 'rd' : 'th';
  22110. return number + output;
  22111. }
  22112. });
  22113. /* EMBED_LANGUAGES */
  22114. /************************************
  22115. Exposing Moment
  22116. ************************************/
  22117. function makeGlobal(shouldDeprecate) {
  22118. /*global ender:false */
  22119. if (typeof ender !== 'undefined') {
  22120. return;
  22121. }
  22122. oldGlobalMoment = globalScope.moment;
  22123. if (shouldDeprecate) {
  22124. globalScope.moment = deprecate(
  22125. "Accessing Moment through the global scope is " +
  22126. "deprecated, and will be removed in an upcoming " +
  22127. "release.",
  22128. moment);
  22129. } else {
  22130. globalScope.moment = moment;
  22131. }
  22132. }
  22133. // CommonJS module is defined
  22134. if (hasModule) {
  22135. module.exports = moment;
  22136. } else if (typeof define === "function" && define.amd) {
  22137. define("moment", function (require, exports, module) {
  22138. if (module.config && module.config() && module.config().noGlobal === true) {
  22139. // release the global variable
  22140. globalScope.moment = oldGlobalMoment;
  22141. }
  22142. return moment;
  22143. });
  22144. makeGlobal(true);
  22145. } else {
  22146. makeGlobal();
  22147. }
  22148. }).call(this);
  22149. },{}],5:[function(require,module,exports){
  22150. /**
  22151. * Copyright 2012 Craig Campbell
  22152. *
  22153. * Licensed under the Apache License, Version 2.0 (the "License");
  22154. * you may not use this file except in compliance with the License.
  22155. * You may obtain a copy of the License at
  22156. *
  22157. * http://www.apache.org/licenses/LICENSE-2.0
  22158. *
  22159. * Unless required by applicable law or agreed to in writing, software
  22160. * distributed under the License is distributed on an "AS IS" BASIS,
  22161. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  22162. * See the License for the specific language governing permissions and
  22163. * limitations under the License.
  22164. *
  22165. * Mousetrap is a simple keyboard shortcut library for Javascript with
  22166. * no external dependencies
  22167. *
  22168. * @version 1.1.2
  22169. * @url craig.is/killing/mice
  22170. */
  22171. /**
  22172. * mapping of special keycodes to their corresponding keys
  22173. *
  22174. * everything in this dictionary cannot use keypress events
  22175. * so it has to be here to map to the correct keycodes for
  22176. * keyup/keydown events
  22177. *
  22178. * @type {Object}
  22179. */
  22180. var _MAP = {
  22181. 8: 'backspace',
  22182. 9: 'tab',
  22183. 13: 'enter',
  22184. 16: 'shift',
  22185. 17: 'ctrl',
  22186. 18: 'alt',
  22187. 20: 'capslock',
  22188. 27: 'esc',
  22189. 32: 'space',
  22190. 33: 'pageup',
  22191. 34: 'pagedown',
  22192. 35: 'end',
  22193. 36: 'home',
  22194. 37: 'left',
  22195. 38: 'up',
  22196. 39: 'right',
  22197. 40: 'down',
  22198. 45: 'ins',
  22199. 46: 'del',
  22200. 91: 'meta',
  22201. 93: 'meta',
  22202. 224: 'meta'
  22203. },
  22204. /**
  22205. * mapping for special characters so they can support
  22206. *
  22207. * this dictionary is only used incase you want to bind a
  22208. * keyup or keydown event to one of these keys
  22209. *
  22210. * @type {Object}
  22211. */
  22212. _KEYCODE_MAP = {
  22213. 106: '*',
  22214. 107: '+',
  22215. 109: '-',
  22216. 110: '.',
  22217. 111 : '/',
  22218. 186: ';',
  22219. 187: '=',
  22220. 188: ',',
  22221. 189: '-',
  22222. 190: '.',
  22223. 191: '/',
  22224. 192: '`',
  22225. 219: '[',
  22226. 220: '\\',
  22227. 221: ']',
  22228. 222: '\''
  22229. },
  22230. /**
  22231. * this is a mapping of keys that require shift on a US keypad
  22232. * back to the non shift equivelents
  22233. *
  22234. * this is so you can use keyup events with these keys
  22235. *
  22236. * note that this will only work reliably on US keyboards
  22237. *
  22238. * @type {Object}
  22239. */
  22240. _SHIFT_MAP = {
  22241. '~': '`',
  22242. '!': '1',
  22243. '@': '2',
  22244. '#': '3',
  22245. '$': '4',
  22246. '%': '5',
  22247. '^': '6',
  22248. '&': '7',
  22249. '*': '8',
  22250. '(': '9',
  22251. ')': '0',
  22252. '_': '-',
  22253. '+': '=',
  22254. ':': ';',
  22255. '\"': '\'',
  22256. '<': ',',
  22257. '>': '.',
  22258. '?': '/',
  22259. '|': '\\'
  22260. },
  22261. /**
  22262. * this is a list of special strings you can use to map
  22263. * to modifier keys when you specify your keyboard shortcuts
  22264. *
  22265. * @type {Object}
  22266. */
  22267. _SPECIAL_ALIASES = {
  22268. 'option': 'alt',
  22269. 'command': 'meta',
  22270. 'return': 'enter',
  22271. 'escape': 'esc'
  22272. },
  22273. /**
  22274. * variable to store the flipped version of _MAP from above
  22275. * needed to check if we should use keypress or not when no action
  22276. * is specified
  22277. *
  22278. * @type {Object|undefined}
  22279. */
  22280. _REVERSE_MAP,
  22281. /**
  22282. * a list of all the callbacks setup via Mousetrap.bind()
  22283. *
  22284. * @type {Object}
  22285. */
  22286. _callbacks = {},
  22287. /**
  22288. * direct map of string combinations to callbacks used for trigger()
  22289. *
  22290. * @type {Object}
  22291. */
  22292. _direct_map = {},
  22293. /**
  22294. * keeps track of what level each sequence is at since multiple
  22295. * sequences can start out with the same sequence
  22296. *
  22297. * @type {Object}
  22298. */
  22299. _sequence_levels = {},
  22300. /**
  22301. * variable to store the setTimeout call
  22302. *
  22303. * @type {null|number}
  22304. */
  22305. _reset_timer,
  22306. /**
  22307. * temporary state where we will ignore the next keyup
  22308. *
  22309. * @type {boolean|string}
  22310. */
  22311. _ignore_next_keyup = false,
  22312. /**
  22313. * are we currently inside of a sequence?
  22314. * type of action ("keyup" or "keydown" or "keypress") or false
  22315. *
  22316. * @type {boolean|string}
  22317. */
  22318. _inside_sequence = false;
  22319. /**
  22320. * loop through the f keys, f1 to f19 and add them to the map
  22321. * programatically
  22322. */
  22323. for (var i = 1; i < 20; ++i) {
  22324. _MAP[111 + i] = 'f' + i;
  22325. }
  22326. /**
  22327. * loop through to map numbers on the numeric keypad
  22328. */
  22329. for (i = 0; i <= 9; ++i) {
  22330. _MAP[i + 96] = i;
  22331. }
  22332. /**
  22333. * cross browser add event method
  22334. *
  22335. * @param {Element|HTMLDocument} object
  22336. * @param {string} type
  22337. * @param {Function} callback
  22338. * @returns void
  22339. */
  22340. function _addEvent(object, type, callback) {
  22341. if (object.addEventListener) {
  22342. return object.addEventListener(type, callback, false);
  22343. }
  22344. object.attachEvent('on' + type, callback);
  22345. }
  22346. /**
  22347. * takes the event and returns the key character
  22348. *
  22349. * @param {Event} e
  22350. * @return {string}
  22351. */
  22352. function _characterFromEvent(e) {
  22353. // for keypress events we should return the character as is
  22354. if (e.type == 'keypress') {
  22355. return String.fromCharCode(e.which);
  22356. }
  22357. // for non keypress events the special maps are needed
  22358. if (_MAP[e.which]) {
  22359. return _MAP[e.which];
  22360. }
  22361. if (_KEYCODE_MAP[e.which]) {
  22362. return _KEYCODE_MAP[e.which];
  22363. }
  22364. // if it is not in the special map
  22365. return String.fromCharCode(e.which).toLowerCase();
  22366. }
  22367. /**
  22368. * should we stop this event before firing off callbacks
  22369. *
  22370. * @param {Event} e
  22371. * @return {boolean}
  22372. */
  22373. function _stop(e) {
  22374. var element = e.target || e.srcElement,
  22375. tag_name = element.tagName;
  22376. // if the element has the class "mousetrap" then no need to stop
  22377. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  22378. return false;
  22379. }
  22380. // stop for input, select, and textarea
  22381. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  22382. }
  22383. /**
  22384. * checks if two arrays are equal
  22385. *
  22386. * @param {Array} modifiers1
  22387. * @param {Array} modifiers2
  22388. * @returns {boolean}
  22389. */
  22390. function _modifiersMatch(modifiers1, modifiers2) {
  22391. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  22392. }
  22393. /**
  22394. * resets all sequence counters except for the ones passed in
  22395. *
  22396. * @param {Object} do_not_reset
  22397. * @returns void
  22398. */
  22399. function _resetSequences(do_not_reset) {
  22400. do_not_reset = do_not_reset || {};
  22401. var active_sequences = false,
  22402. key;
  22403. for (key in _sequence_levels) {
  22404. if (do_not_reset[key]) {
  22405. active_sequences = true;
  22406. continue;
  22407. }
  22408. _sequence_levels[key] = 0;
  22409. }
  22410. if (!active_sequences) {
  22411. _inside_sequence = false;
  22412. }
  22413. }
  22414. /**
  22415. * finds all callbacks that match based on the keycode, modifiers,
  22416. * and action
  22417. *
  22418. * @param {string} character
  22419. * @param {Array} modifiers
  22420. * @param {string} action
  22421. * @param {boolean=} remove - should we remove any matches
  22422. * @param {string=} combination
  22423. * @returns {Array}
  22424. */
  22425. function _getMatches(character, modifiers, action, remove, combination) {
  22426. var i,
  22427. callback,
  22428. matches = [];
  22429. // if there are no events related to this keycode
  22430. if (!_callbacks[character]) {
  22431. return [];
  22432. }
  22433. // if a modifier key is coming up on its own we should allow it
  22434. if (action == 'keyup' && _isModifier(character)) {
  22435. modifiers = [character];
  22436. }
  22437. // loop through all callbacks for the key that was pressed
  22438. // and see if any of them match
  22439. for (i = 0; i < _callbacks[character].length; ++i) {
  22440. callback = _callbacks[character][i];
  22441. // if this is a sequence but it is not at the right level
  22442. // then move onto the next match
  22443. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  22444. continue;
  22445. }
  22446. // if the action we are looking for doesn't match the action we got
  22447. // then we should keep going
  22448. if (action != callback.action) {
  22449. continue;
  22450. }
  22451. // if this is a keypress event that means that we need to only
  22452. // look at the character, otherwise check the modifiers as
  22453. // well
  22454. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  22455. // remove is used so if you change your mind and call bind a
  22456. // second time with a new function the first one is overwritten
  22457. if (remove && callback.combo == combination) {
  22458. _callbacks[character].splice(i, 1);
  22459. }
  22460. matches.push(callback);
  22461. }
  22462. }
  22463. return matches;
  22464. }
  22465. /**
  22466. * takes a key event and figures out what the modifiers are
  22467. *
  22468. * @param {Event} e
  22469. * @returns {Array}
  22470. */
  22471. function _eventModifiers(e) {
  22472. var modifiers = [];
  22473. if (e.shiftKey) {
  22474. modifiers.push('shift');
  22475. }
  22476. if (e.altKey) {
  22477. modifiers.push('alt');
  22478. }
  22479. if (e.ctrlKey) {
  22480. modifiers.push('ctrl');
  22481. }
  22482. if (e.metaKey) {
  22483. modifiers.push('meta');
  22484. }
  22485. return modifiers;
  22486. }
  22487. /**
  22488. * actually calls the callback function
  22489. *
  22490. * if your callback function returns false this will use the jquery
  22491. * convention - prevent default and stop propogation on the event
  22492. *
  22493. * @param {Function} callback
  22494. * @param {Event} e
  22495. * @returns void
  22496. */
  22497. function _fireCallback(callback, e) {
  22498. if (callback(e) === false) {
  22499. if (e.preventDefault) {
  22500. e.preventDefault();
  22501. }
  22502. if (e.stopPropagation) {
  22503. e.stopPropagation();
  22504. }
  22505. e.returnValue = false;
  22506. e.cancelBubble = true;
  22507. }
  22508. }
  22509. /**
  22510. * handles a character key event
  22511. *
  22512. * @param {string} character
  22513. * @param {Event} e
  22514. * @returns void
  22515. */
  22516. function _handleCharacter(character, e) {
  22517. // if this event should not happen stop here
  22518. if (_stop(e)) {
  22519. return;
  22520. }
  22521. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  22522. i,
  22523. do_not_reset = {},
  22524. processed_sequence_callback = false;
  22525. // loop through matching callbacks for this key event
  22526. for (i = 0; i < callbacks.length; ++i) {
  22527. // fire for all sequence callbacks
  22528. // this is because if for example you have multiple sequences
  22529. // bound such as "g i" and "g t" they both need to fire the
  22530. // callback for matching g cause otherwise you can only ever
  22531. // match the first one
  22532. if (callbacks[i].seq) {
  22533. processed_sequence_callback = true;
  22534. // keep a list of which sequences were matches for later
  22535. do_not_reset[callbacks[i].seq] = 1;
  22536. _fireCallback(callbacks[i].callback, e);
  22537. continue;
  22538. }
  22539. // if there were no sequence matches but we are still here
  22540. // that means this is a regular match so we should fire that
  22541. if (!processed_sequence_callback && !_inside_sequence) {
  22542. _fireCallback(callbacks[i].callback, e);
  22543. }
  22544. }
  22545. // if you are inside of a sequence and the key you are pressing
  22546. // is not a modifier key then we should reset all sequences
  22547. // that were not matched by this key event
  22548. if (e.type == _inside_sequence && !_isModifier(character)) {
  22549. _resetSequences(do_not_reset);
  22550. }
  22551. }
  22552. /**
  22553. * handles a keydown event
  22554. *
  22555. * @param {Event} e
  22556. * @returns void
  22557. */
  22558. function _handleKey(e) {
  22559. // normalize e.which for key events
  22560. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  22561. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  22562. var character = _characterFromEvent(e);
  22563. // no character found then stop
  22564. if (!character) {
  22565. return;
  22566. }
  22567. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  22568. _ignore_next_keyup = false;
  22569. return;
  22570. }
  22571. _handleCharacter(character, e);
  22572. }
  22573. /**
  22574. * determines if the keycode specified is a modifier key or not
  22575. *
  22576. * @param {string} key
  22577. * @returns {boolean}
  22578. */
  22579. function _isModifier(key) {
  22580. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  22581. }
  22582. /**
  22583. * called to set a 1 second timeout on the specified sequence
  22584. *
  22585. * this is so after each key press in the sequence you have 1 second
  22586. * to press the next key before you have to start over
  22587. *
  22588. * @returns void
  22589. */
  22590. function _resetSequenceTimer() {
  22591. clearTimeout(_reset_timer);
  22592. _reset_timer = setTimeout(_resetSequences, 1000);
  22593. }
  22594. /**
  22595. * reverses the map lookup so that we can look for specific keys
  22596. * to see what can and can't use keypress
  22597. *
  22598. * @return {Object}
  22599. */
  22600. function _getReverseMap() {
  22601. if (!_REVERSE_MAP) {
  22602. _REVERSE_MAP = {};
  22603. for (var key in _MAP) {
  22604. // pull out the numeric keypad from here cause keypress should
  22605. // be able to detect the keys from the character
  22606. if (key > 95 && key < 112) {
  22607. continue;
  22608. }
  22609. if (_MAP.hasOwnProperty(key)) {
  22610. _REVERSE_MAP[_MAP[key]] = key;
  22611. }
  22612. }
  22613. }
  22614. return _REVERSE_MAP;
  22615. }
  22616. /**
  22617. * picks the best action based on the key combination
  22618. *
  22619. * @param {string} key - character for key
  22620. * @param {Array} modifiers
  22621. * @param {string=} action passed in
  22622. */
  22623. function _pickBestAction(key, modifiers, action) {
  22624. // if no action was picked in we should try to pick the one
  22625. // that we think would work best for this key
  22626. if (!action) {
  22627. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  22628. }
  22629. // modifier keys don't work as expected with keypress,
  22630. // switch to keydown
  22631. if (action == 'keypress' && modifiers.length) {
  22632. action = 'keydown';
  22633. }
  22634. return action;
  22635. }
  22636. /**
  22637. * binds a key sequence to an event
  22638. *
  22639. * @param {string} combo - combo specified in bind call
  22640. * @param {Array} keys
  22641. * @param {Function} callback
  22642. * @param {string=} action
  22643. * @returns void
  22644. */
  22645. function _bindSequence(combo, keys, callback, action) {
  22646. // start off by adding a sequence level record for this combination
  22647. // and setting the level to 0
  22648. _sequence_levels[combo] = 0;
  22649. // if there is no action pick the best one for the first key
  22650. // in the sequence
  22651. if (!action) {
  22652. action = _pickBestAction(keys[0], []);
  22653. }
  22654. /**
  22655. * callback to increase the sequence level for this sequence and reset
  22656. * all other sequences that were active
  22657. *
  22658. * @param {Event} e
  22659. * @returns void
  22660. */
  22661. var _increaseSequence = function(e) {
  22662. _inside_sequence = action;
  22663. ++_sequence_levels[combo];
  22664. _resetSequenceTimer();
  22665. },
  22666. /**
  22667. * wraps the specified callback inside of another function in order
  22668. * to reset all sequence counters as soon as this sequence is done
  22669. *
  22670. * @param {Event} e
  22671. * @returns void
  22672. */
  22673. _callbackAndReset = function(e) {
  22674. _fireCallback(callback, e);
  22675. // we should ignore the next key up if the action is key down
  22676. // or keypress. this is so if you finish a sequence and
  22677. // release the key the final key will not trigger a keyup
  22678. if (action !== 'keyup') {
  22679. _ignore_next_keyup = _characterFromEvent(e);
  22680. }
  22681. // weird race condition if a sequence ends with the key
  22682. // another sequence begins with
  22683. setTimeout(_resetSequences, 10);
  22684. },
  22685. i;
  22686. // loop through keys one at a time and bind the appropriate callback
  22687. // function. for any key leading up to the final one it should
  22688. // increase the sequence. after the final, it should reset all sequences
  22689. for (i = 0; i < keys.length; ++i) {
  22690. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  22691. }
  22692. }
  22693. /**
  22694. * binds a single keyboard combination
  22695. *
  22696. * @param {string} combination
  22697. * @param {Function} callback
  22698. * @param {string=} action
  22699. * @param {string=} sequence_name - name of sequence if part of sequence
  22700. * @param {number=} level - what part of the sequence the command is
  22701. * @returns void
  22702. */
  22703. function _bindSingle(combination, callback, action, sequence_name, level) {
  22704. // make sure multiple spaces in a row become a single space
  22705. combination = combination.replace(/\s+/g, ' ');
  22706. var sequence = combination.split(' '),
  22707. i,
  22708. key,
  22709. keys,
  22710. modifiers = [];
  22711. // if this pattern is a sequence of keys then run through this method
  22712. // to reprocess each pattern one key at a time
  22713. if (sequence.length > 1) {
  22714. return _bindSequence(combination, sequence, callback, action);
  22715. }
  22716. // take the keys from this pattern and figure out what the actual
  22717. // pattern is all about
  22718. keys = combination === '+' ? ['+'] : combination.split('+');
  22719. for (i = 0; i < keys.length; ++i) {
  22720. key = keys[i];
  22721. // normalize key names
  22722. if (_SPECIAL_ALIASES[key]) {
  22723. key = _SPECIAL_ALIASES[key];
  22724. }
  22725. // if this is not a keypress event then we should
  22726. // be smart about using shift keys
  22727. // this will only work for US keyboards however
  22728. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  22729. key = _SHIFT_MAP[key];
  22730. modifiers.push('shift');
  22731. }
  22732. // if this key is a modifier then add it to the list of modifiers
  22733. if (_isModifier(key)) {
  22734. modifiers.push(key);
  22735. }
  22736. }
  22737. // depending on what the key combination is
  22738. // we will try to pick the best event for it
  22739. action = _pickBestAction(key, modifiers, action);
  22740. // make sure to initialize array if this is the first time
  22741. // a callback is added for this key
  22742. if (!_callbacks[key]) {
  22743. _callbacks[key] = [];
  22744. }
  22745. // remove an existing match if there is one
  22746. _getMatches(key, modifiers, action, !sequence_name, combination);
  22747. // add this call back to the array
  22748. // if it is a sequence put it at the beginning
  22749. // if not put it at the end
  22750. //
  22751. // this is important because the way these are processed expects
  22752. // the sequence ones to come first
  22753. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  22754. callback: callback,
  22755. modifiers: modifiers,
  22756. action: action,
  22757. seq: sequence_name,
  22758. level: level,
  22759. combo: combination
  22760. });
  22761. }
  22762. /**
  22763. * binds multiple combinations to the same callback
  22764. *
  22765. * @param {Array} combinations
  22766. * @param {Function} callback
  22767. * @param {string|undefined} action
  22768. * @returns void
  22769. */
  22770. function _bindMultiple(combinations, callback, action) {
  22771. for (var i = 0; i < combinations.length; ++i) {
  22772. _bindSingle(combinations[i], callback, action);
  22773. }
  22774. }
  22775. // start!
  22776. _addEvent(document, 'keypress', _handleKey);
  22777. _addEvent(document, 'keydown', _handleKey);
  22778. _addEvent(document, 'keyup', _handleKey);
  22779. var mousetrap = {
  22780. /**
  22781. * binds an event to mousetrap
  22782. *
  22783. * can be a single key, a combination of keys separated with +,
  22784. * a comma separated list of keys, an array of keys, or
  22785. * a sequence of keys separated by spaces
  22786. *
  22787. * be sure to list the modifier keys first to make sure that the
  22788. * correct key ends up getting bound (the last key in the pattern)
  22789. *
  22790. * @param {string|Array} keys
  22791. * @param {Function} callback
  22792. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  22793. * @returns void
  22794. */
  22795. bind: function(keys, callback, action) {
  22796. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  22797. _direct_map[keys + ':' + action] = callback;
  22798. return this;
  22799. },
  22800. /**
  22801. * unbinds an event to mousetrap
  22802. *
  22803. * the unbinding sets the callback function of the specified key combo
  22804. * to an empty function and deletes the corresponding key in the
  22805. * _direct_map dict.
  22806. *
  22807. * the keycombo+action has to be exactly the same as
  22808. * it was defined in the bind method
  22809. *
  22810. * TODO: actually remove this from the _callbacks dictionary instead
  22811. * of binding an empty function
  22812. *
  22813. * @param {string|Array} keys
  22814. * @param {string} action
  22815. * @returns void
  22816. */
  22817. unbind: function(keys, action) {
  22818. if (_direct_map[keys + ':' + action]) {
  22819. delete _direct_map[keys + ':' + action];
  22820. this.bind(keys, function() {}, action);
  22821. }
  22822. return this;
  22823. },
  22824. /**
  22825. * triggers an event that has already been bound
  22826. *
  22827. * @param {string} keys
  22828. * @param {string=} action
  22829. * @returns void
  22830. */
  22831. trigger: function(keys, action) {
  22832. _direct_map[keys + ':' + action]();
  22833. return this;
  22834. },
  22835. /**
  22836. * resets the library back to its initial state. this is useful
  22837. * if you want to clear out the current keyboard shortcuts and bind
  22838. * new ones - for example if you switch to another page
  22839. *
  22840. * @returns void
  22841. */
  22842. reset: function() {
  22843. _callbacks = {};
  22844. _direct_map = {};
  22845. return this;
  22846. }
  22847. };
  22848. module.exports = mousetrap;
  22849. },{}]},{},[1])
  22850. (1)
  22851. });