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.

26299 lines
776 KiB

  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-06
  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 = 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.65; // 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. moveable: true,
  13958. zoomable: true,
  13959. hover: false
  13960. };
  13961. this.hoverObj = {nodes:{},edges:{}};
  13962. this.editMode = this.constants.dataManipulation.initiallyVisible;
  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.moveable !== undefined) {this.constants.moveable = options.moveable;}
  14255. if (options.zoomable !== undefined) {this.constants.zoomable = options.zoomable;}
  14256. if (options.hover !== undefined) {this.constants.hover = options.hover;}
  14257. if (options.labels !== undefined) {
  14258. for (prop in options.labels) {
  14259. if (options.labels.hasOwnProperty(prop)) {
  14260. this.constants.labels[prop] = options.labels[prop];
  14261. }
  14262. }
  14263. }
  14264. if (options.onAdd) {
  14265. this.triggerFunctions.add = options.onAdd;
  14266. }
  14267. if (options.onEdit) {
  14268. this.triggerFunctions.edit = options.onEdit;
  14269. }
  14270. if (options.onConnect) {
  14271. this.triggerFunctions.connect = options.onConnect;
  14272. }
  14273. if (options.onDelete) {
  14274. this.triggerFunctions.del = options.onDelete;
  14275. }
  14276. if (options.physics) {
  14277. if (options.physics.barnesHut) {
  14278. this.constants.physics.barnesHut.enabled = true;
  14279. for (prop in options.physics.barnesHut) {
  14280. if (options.physics.barnesHut.hasOwnProperty(prop)) {
  14281. this.constants.physics.barnesHut[prop] = options.physics.barnesHut[prop];
  14282. }
  14283. }
  14284. }
  14285. if (options.physics.repulsion) {
  14286. this.constants.physics.barnesHut.enabled = false;
  14287. for (prop in options.physics.repulsion) {
  14288. if (options.physics.repulsion.hasOwnProperty(prop)) {
  14289. this.constants.physics.repulsion[prop] = options.physics.repulsion[prop];
  14290. }
  14291. }
  14292. }
  14293. if (options.physics.hierarchicalRepulsion) {
  14294. this.constants.hierarchicalLayout.enabled = true;
  14295. this.constants.physics.hierarchicalRepulsion.enabled = true;
  14296. this.constants.physics.barnesHut.enabled = false;
  14297. for (prop in options.physics.hierarchicalRepulsion) {
  14298. if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
  14299. this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
  14300. }
  14301. }
  14302. }
  14303. }
  14304. if (options.hierarchicalLayout) {
  14305. this.constants.hierarchicalLayout.enabled = true;
  14306. for (prop in options.hierarchicalLayout) {
  14307. if (options.hierarchicalLayout.hasOwnProperty(prop)) {
  14308. this.constants.hierarchicalLayout[prop] = options.hierarchicalLayout[prop];
  14309. }
  14310. }
  14311. }
  14312. else if (options.hierarchicalLayout !== undefined) {
  14313. this.constants.hierarchicalLayout.enabled = false;
  14314. }
  14315. if (options.clustering) {
  14316. this.constants.clustering.enabled = true;
  14317. for (prop in options.clustering) {
  14318. if (options.clustering.hasOwnProperty(prop)) {
  14319. this.constants.clustering[prop] = options.clustering[prop];
  14320. }
  14321. }
  14322. }
  14323. else if (options.clustering !== undefined) {
  14324. this.constants.clustering.enabled = false;
  14325. }
  14326. if (options.navigation) {
  14327. this.constants.navigation.enabled = true;
  14328. for (prop in options.navigation) {
  14329. if (options.navigation.hasOwnProperty(prop)) {
  14330. this.constants.navigation[prop] = options.navigation[prop];
  14331. }
  14332. }
  14333. }
  14334. else if (options.navigation !== undefined) {
  14335. this.constants.navigation.enabled = false;
  14336. }
  14337. if (options.keyboard) {
  14338. this.constants.keyboard.enabled = true;
  14339. for (prop in options.keyboard) {
  14340. if (options.keyboard.hasOwnProperty(prop)) {
  14341. this.constants.keyboard[prop] = options.keyboard[prop];
  14342. }
  14343. }
  14344. }
  14345. else if (options.keyboard !== undefined) {
  14346. this.constants.keyboard.enabled = false;
  14347. }
  14348. if (options.dataManipulation) {
  14349. this.constants.dataManipulation.enabled = true;
  14350. for (prop in options.dataManipulation) {
  14351. if (options.dataManipulation.hasOwnProperty(prop)) {
  14352. this.constants.dataManipulation[prop] = options.dataManipulation[prop];
  14353. }
  14354. }
  14355. }
  14356. else if (options.dataManipulation !== undefined) {
  14357. this.constants.dataManipulation.enabled = false;
  14358. }
  14359. // TODO: work out these options and document them
  14360. if (options.edges) {
  14361. for (prop in options.edges) {
  14362. if (options.edges.hasOwnProperty(prop)) {
  14363. if (typeof options.edges[prop] != "object") {
  14364. this.constants.edges[prop] = options.edges[prop];
  14365. }
  14366. }
  14367. }
  14368. if (options.edges.color !== undefined) {
  14369. if (util.isString(options.edges.color)) {
  14370. this.constants.edges.color = {};
  14371. this.constants.edges.color.color = options.edges.color;
  14372. this.constants.edges.color.highlight = options.edges.color;
  14373. this.constants.edges.color.hover = options.edges.color;
  14374. }
  14375. else {
  14376. if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
  14377. if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
  14378. if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
  14379. }
  14380. }
  14381. if (!options.edges.fontColor) {
  14382. if (options.edges.color !== undefined) {
  14383. if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
  14384. else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
  14385. }
  14386. }
  14387. // Added to support dashed lines
  14388. // David Jordan
  14389. // 2012-08-08
  14390. if (options.edges.dash) {
  14391. if (options.edges.dash.length !== undefined) {
  14392. this.constants.edges.dash.length = options.edges.dash.length;
  14393. }
  14394. if (options.edges.dash.gap !== undefined) {
  14395. this.constants.edges.dash.gap = options.edges.dash.gap;
  14396. }
  14397. if (options.edges.dash.altLength !== undefined) {
  14398. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  14399. }
  14400. }
  14401. }
  14402. if (options.nodes) {
  14403. for (prop in options.nodes) {
  14404. if (options.nodes.hasOwnProperty(prop)) {
  14405. this.constants.nodes[prop] = options.nodes[prop];
  14406. }
  14407. }
  14408. if (options.nodes.color) {
  14409. this.constants.nodes.color = util.parseColor(options.nodes.color);
  14410. }
  14411. /*
  14412. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  14413. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  14414. */
  14415. }
  14416. if (options.groups) {
  14417. for (var groupname in options.groups) {
  14418. if (options.groups.hasOwnProperty(groupname)) {
  14419. var group = options.groups[groupname];
  14420. this.groups.add(groupname, group);
  14421. }
  14422. }
  14423. }
  14424. if (options.tooltip) {
  14425. for (prop in options.tooltip) {
  14426. if (options.tooltip.hasOwnProperty(prop)) {
  14427. this.constants.tooltip[prop] = options.tooltip[prop];
  14428. }
  14429. }
  14430. if (options.tooltip.color) {
  14431. this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  14432. }
  14433. }
  14434. }
  14435. // (Re)loading the mixins that can be enabled or disabled in the options.
  14436. // load the force calculation functions, grouped under the physics system.
  14437. this._loadPhysicsSystem();
  14438. // load the navigation system.
  14439. this._loadNavigationControls();
  14440. // load the data manipulation system
  14441. this._loadManipulationSystem();
  14442. // configure the smooth curves
  14443. this._configureSmoothCurves();
  14444. // bind keys. If disabled, this will not do anything;
  14445. this._createKeyBinds();
  14446. this.setSize(this.width, this.height);
  14447. this.moving = true;
  14448. this.start();
  14449. };
  14450. /**
  14451. * Create the main frame for the Graph.
  14452. * This function is executed once when a Graph object is created. The frame
  14453. * contains a canvas, and this canvas contains all objects like the axis and
  14454. * nodes.
  14455. * @private
  14456. */
  14457. Graph.prototype._create = function () {
  14458. // remove all elements from the container element.
  14459. while (this.containerElement.hasChildNodes()) {
  14460. this.containerElement.removeChild(this.containerElement.firstChild);
  14461. }
  14462. this.frame = document.createElement('div');
  14463. this.frame.className = 'graph-frame';
  14464. this.frame.style.position = 'relative';
  14465. this.frame.style.overflow = 'hidden';
  14466. // create the graph canvas (HTML canvas element)
  14467. this.frame.canvas = document.createElement( 'canvas' );
  14468. this.frame.canvas.style.position = 'relative';
  14469. this.frame.appendChild(this.frame.canvas);
  14470. if (!this.frame.canvas.getContext) {
  14471. var noCanvas = document.createElement( 'DIV' );
  14472. noCanvas.style.color = 'red';
  14473. noCanvas.style.fontWeight = 'bold' ;
  14474. noCanvas.style.padding = '10px';
  14475. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14476. this.frame.canvas.appendChild(noCanvas);
  14477. }
  14478. var me = this;
  14479. this.drag = {};
  14480. this.pinch = {};
  14481. this.hammer = Hammer(this.frame.canvas, {
  14482. prevent_default: true
  14483. });
  14484. this.hammer.on('tap', me._onTap.bind(me) );
  14485. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  14486. this.hammer.on('hold', me._onHold.bind(me) );
  14487. this.hammer.on('pinch', me._onPinch.bind(me) );
  14488. this.hammer.on('touch', me._onTouch.bind(me) );
  14489. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14490. this.hammer.on('drag', me._onDrag.bind(me) );
  14491. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14492. this.hammer.on('release', me._onRelease.bind(me) );
  14493. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14494. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14495. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14496. // add the frame to the container element
  14497. this.containerElement.appendChild(this.frame);
  14498. };
  14499. /**
  14500. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  14501. * @private
  14502. */
  14503. Graph.prototype._createKeyBinds = function() {
  14504. var me = this;
  14505. this.mousetrap = mousetrap;
  14506. this.mousetrap.reset();
  14507. if (this.constants.keyboard.enabled == true) {
  14508. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  14509. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  14510. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  14511. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  14512. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  14513. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  14514. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  14515. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  14516. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  14517. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  14518. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  14519. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  14520. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  14521. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  14522. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  14523. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  14524. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  14525. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  14526. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  14527. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  14528. }
  14529. if (this.constants.dataManipulation.enabled == true) {
  14530. this.mousetrap.bind("escape",this._createManipulatorBar.bind(me));
  14531. this.mousetrap.bind("del",this._deleteSelected.bind(me));
  14532. }
  14533. };
  14534. /**
  14535. * Get the pointer location from a touch location
  14536. * @param {{pageX: Number, pageY: Number}} touch
  14537. * @return {{x: Number, y: Number}} pointer
  14538. * @private
  14539. */
  14540. Graph.prototype._getPointer = function (touch) {
  14541. return {
  14542. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14543. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14544. };
  14545. };
  14546. /**
  14547. * On start of a touch gesture, store the pointer
  14548. * @param event
  14549. * @private
  14550. */
  14551. Graph.prototype._onTouch = function (event) {
  14552. this.drag.pointer = this._getPointer(event.gesture.center);
  14553. this.drag.pinched = false;
  14554. this.pinch.scale = this._getScale();
  14555. this._handleTouch(this.drag.pointer);
  14556. };
  14557. /**
  14558. * handle drag start event
  14559. * @private
  14560. */
  14561. Graph.prototype._onDragStart = function () {
  14562. this._handleDragStart();
  14563. };
  14564. /**
  14565. * This function is called by _onDragStart.
  14566. * It is separated out because we can then overload it for the datamanipulation system.
  14567. *
  14568. * @private
  14569. */
  14570. Graph.prototype._handleDragStart = function() {
  14571. var drag = this.drag;
  14572. var node = this._getNodeAt(drag.pointer);
  14573. // note: drag.pointer is set in _onTouch to get the initial touch location
  14574. drag.dragging = true;
  14575. drag.selection = [];
  14576. drag.translation = this._getTranslation();
  14577. drag.nodeId = null;
  14578. if (node != null) {
  14579. drag.nodeId = node.id;
  14580. // select the clicked node if not yet selected
  14581. if (!node.isSelected()) {
  14582. this._selectObject(node,false);
  14583. }
  14584. // create an array with the selected nodes and their original location and status
  14585. for (var objectId in this.selectionObj.nodes) {
  14586. if (this.selectionObj.nodes.hasOwnProperty(objectId)) {
  14587. var object = this.selectionObj.nodes[objectId];
  14588. var s = {
  14589. id: object.id,
  14590. node: object,
  14591. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14592. x: object.x,
  14593. y: object.y,
  14594. xFixed: object.xFixed,
  14595. yFixed: object.yFixed
  14596. };
  14597. object.xFixed = true;
  14598. object.yFixed = true;
  14599. drag.selection.push(s);
  14600. }
  14601. }
  14602. }
  14603. };
  14604. /**
  14605. * handle drag event
  14606. * @private
  14607. */
  14608. Graph.prototype._onDrag = function (event) {
  14609. this._handleOnDrag(event)
  14610. };
  14611. /**
  14612. * This function is called by _onDrag.
  14613. * It is separated out because we can then overload it for the datamanipulation system.
  14614. *
  14615. * @private
  14616. */
  14617. Graph.prototype._handleOnDrag = function(event) {
  14618. if (this.drag.pinched) {
  14619. return;
  14620. }
  14621. var pointer = this._getPointer(event.gesture.center);
  14622. var me = this,
  14623. drag = this.drag,
  14624. selection = drag.selection;
  14625. if (selection && selection.length) {
  14626. // calculate delta's and new location
  14627. var deltaX = pointer.x - drag.pointer.x,
  14628. deltaY = pointer.y - drag.pointer.y;
  14629. // update position of all selected nodes
  14630. selection.forEach(function (s) {
  14631. var node = s.node;
  14632. if (!s.xFixed) {
  14633. node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX);
  14634. }
  14635. if (!s.yFixed) {
  14636. node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY);
  14637. }
  14638. });
  14639. // start _animationStep if not yet running
  14640. if (!this.moving) {
  14641. this.moving = true;
  14642. this.start();
  14643. }
  14644. }
  14645. else {
  14646. if (this.constants.moveable == true) {
  14647. // move the graph
  14648. var diffX = pointer.x - this.drag.pointer.x;
  14649. var diffY = pointer.y - this.drag.pointer.y;
  14650. this._setTranslation(
  14651. this.drag.translation.x + diffX,
  14652. this.drag.translation.y + diffY);
  14653. this._redraw();
  14654. this.moving = true;
  14655. this.start();
  14656. }
  14657. }
  14658. };
  14659. /**
  14660. * handle drag start event
  14661. * @private
  14662. */
  14663. Graph.prototype._onDragEnd = function () {
  14664. this.drag.dragging = false;
  14665. var selection = this.drag.selection;
  14666. if (selection) {
  14667. selection.forEach(function (s) {
  14668. // restore original xFixed and yFixed
  14669. s.node.xFixed = s.xFixed;
  14670. s.node.yFixed = s.yFixed;
  14671. });
  14672. }
  14673. };
  14674. /**
  14675. * handle tap/click event: select/unselect a node
  14676. * @private
  14677. */
  14678. Graph.prototype._onTap = function (event) {
  14679. var pointer = this._getPointer(event.gesture.center);
  14680. this.pointerPosition = pointer;
  14681. this._handleTap(pointer);
  14682. };
  14683. /**
  14684. * handle doubletap event
  14685. * @private
  14686. */
  14687. Graph.prototype._onDoubleTap = function (event) {
  14688. var pointer = this._getPointer(event.gesture.center);
  14689. this._handleDoubleTap(pointer);
  14690. };
  14691. /**
  14692. * handle long tap event: multi select nodes
  14693. * @private
  14694. */
  14695. Graph.prototype._onHold = function (event) {
  14696. var pointer = this._getPointer(event.gesture.center);
  14697. this.pointerPosition = pointer;
  14698. this._handleOnHold(pointer);
  14699. };
  14700. /**
  14701. * handle the release of the screen
  14702. *
  14703. * @private
  14704. */
  14705. Graph.prototype._onRelease = function (event) {
  14706. var pointer = this._getPointer(event.gesture.center);
  14707. this._handleOnRelease(pointer);
  14708. };
  14709. /**
  14710. * Handle pinch event
  14711. * @param event
  14712. * @private
  14713. */
  14714. Graph.prototype._onPinch = function (event) {
  14715. var pointer = this._getPointer(event.gesture.center);
  14716. this.drag.pinched = true;
  14717. if (!('scale' in this.pinch)) {
  14718. this.pinch.scale = 1;
  14719. }
  14720. // TODO: enabled moving while pinching?
  14721. var scale = this.pinch.scale * event.gesture.scale;
  14722. this._zoom(scale, pointer)
  14723. };
  14724. /**
  14725. * Zoom the graph in or out
  14726. * @param {Number} scale a number around 1, and between 0.01 and 10
  14727. * @param {{x: Number, y: Number}} pointer Position on screen
  14728. * @return {Number} appliedScale scale is limited within the boundaries
  14729. * @private
  14730. */
  14731. Graph.prototype._zoom = function(scale, pointer) {
  14732. if (this.constants.zoomable == true) {
  14733. var scaleOld = this._getScale();
  14734. if (scale < 0.00001) {
  14735. scale = 0.00001;
  14736. }
  14737. if (scale > 10) {
  14738. scale = 10;
  14739. }
  14740. // + this.frame.canvas.clientHeight / 2
  14741. var translation = this._getTranslation();
  14742. var scaleFrac = scale / scaleOld;
  14743. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14744. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14745. this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x),
  14746. "y" : this._YconvertDOMtoCanvas(pointer.y)};
  14747. this._setScale(scale);
  14748. this._setTranslation(tx, ty);
  14749. this.updateClustersDefault();
  14750. this._redraw();
  14751. if (scaleOld < scale) {
  14752. this.emit("zoom", {direction:"+"});
  14753. }
  14754. else {
  14755. this.emit("zoom", {direction:"-"});
  14756. }
  14757. return scale;
  14758. }
  14759. };
  14760. /**
  14761. * Event handler for mouse wheel event, used to zoom the timeline
  14762. * See http://adomas.org/javascript-mouse-wheel/
  14763. * https://github.com/EightMedia/hammer.js/issues/256
  14764. * @param {MouseEvent} event
  14765. * @private
  14766. */
  14767. Graph.prototype._onMouseWheel = function(event) {
  14768. // retrieve delta
  14769. var delta = 0;
  14770. if (event.wheelDelta) { /* IE/Opera. */
  14771. delta = event.wheelDelta/120;
  14772. } else if (event.detail) { /* Mozilla case. */
  14773. // In Mozilla, sign of delta is different than in IE.
  14774. // Also, delta is multiple of 3.
  14775. delta = -event.detail/3;
  14776. }
  14777. // If delta is nonzero, handle it.
  14778. // Basically, delta is now positive if wheel was scrolled up,
  14779. // and negative, if wheel was scrolled down.
  14780. if (delta) {
  14781. // calculate the new scale
  14782. var scale = this._getScale();
  14783. var zoom = delta / 10;
  14784. if (delta < 0) {
  14785. zoom = zoom / (1 - zoom);
  14786. }
  14787. scale *= (1 + zoom);
  14788. // calculate the pointer location
  14789. var gesture = util.fakeGesture(this, event);
  14790. var pointer = this._getPointer(gesture.center);
  14791. // apply the new scale
  14792. this._zoom(scale, pointer);
  14793. }
  14794. // Prevent default actions caused by mouse wheel.
  14795. event.preventDefault();
  14796. };
  14797. /**
  14798. * Mouse move handler for checking whether the title moves over a node with a title.
  14799. * @param {Event} event
  14800. * @private
  14801. */
  14802. Graph.prototype._onMouseMoveTitle = function (event) {
  14803. var gesture = util.fakeGesture(this, event);
  14804. var pointer = this._getPointer(gesture.center);
  14805. // check if the previously selected node is still selected
  14806. if (this.popupObj) {
  14807. this._checkHidePopup(pointer);
  14808. }
  14809. // start a timeout that will check if the mouse is positioned above
  14810. // an element
  14811. var me = this;
  14812. var checkShow = function() {
  14813. me._checkShowPopup(pointer);
  14814. };
  14815. if (this.popupTimer) {
  14816. clearInterval(this.popupTimer); // stop any running calculationTimer
  14817. }
  14818. if (!this.drag.dragging) {
  14819. this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
  14820. }
  14821. /**
  14822. * Adding hover highlights
  14823. */
  14824. if (this.constants.hover == true) {
  14825. // removing all hover highlights
  14826. for (var edgeId in this.hoverObj.edges) {
  14827. if (this.hoverObj.edges.hasOwnProperty(edgeId)) {
  14828. this.hoverObj.edges[edgeId].hover = false;
  14829. delete this.hoverObj.edges[edgeId];
  14830. }
  14831. }
  14832. // adding hover highlights
  14833. var obj = this._getNodeAt(pointer);
  14834. if (obj == null) {
  14835. obj = this._getEdgeAt(pointer);
  14836. }
  14837. if (obj != null) {
  14838. this._hoverObject(obj);
  14839. }
  14840. // removing all node hover highlights except for the selected one.
  14841. for (var nodeId in this.hoverObj.nodes) {
  14842. if (this.hoverObj.nodes.hasOwnProperty(nodeId)) {
  14843. if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) {
  14844. this._blurObject(this.hoverObj.nodes[nodeId]);
  14845. delete this.hoverObj.nodes[nodeId];
  14846. }
  14847. }
  14848. }
  14849. this.redraw();
  14850. }
  14851. };
  14852. /**
  14853. * Check if there is an element on the given position in the graph
  14854. * (a node or edge). If so, and if this element has a title,
  14855. * show a popup window with its title.
  14856. *
  14857. * @param {{x:Number, y:Number}} pointer
  14858. * @private
  14859. */
  14860. Graph.prototype._checkShowPopup = function (pointer) {
  14861. var obj = {
  14862. left: this._XconvertDOMtoCanvas(pointer.x),
  14863. top: this._YconvertDOMtoCanvas(pointer.y),
  14864. right: this._XconvertDOMtoCanvas(pointer.x),
  14865. bottom: this._YconvertDOMtoCanvas(pointer.y)
  14866. };
  14867. var id;
  14868. var lastPopupNode = this.popupObj;
  14869. if (this.popupObj == undefined) {
  14870. // search the nodes for overlap, select the top one in case of multiple nodes
  14871. var nodes = this.nodes;
  14872. for (id in nodes) {
  14873. if (nodes.hasOwnProperty(id)) {
  14874. var node = nodes[id];
  14875. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  14876. this.popupObj = node;
  14877. break;
  14878. }
  14879. }
  14880. }
  14881. }
  14882. if (this.popupObj === undefined) {
  14883. // search the edges for overlap
  14884. var edges = this.edges;
  14885. for (id in edges) {
  14886. if (edges.hasOwnProperty(id)) {
  14887. var edge = edges[id];
  14888. if (edge.connected && (edge.getTitle() !== undefined) &&
  14889. edge.isOverlappingWith(obj)) {
  14890. this.popupObj = edge;
  14891. break;
  14892. }
  14893. }
  14894. }
  14895. }
  14896. if (this.popupObj) {
  14897. // show popup message window
  14898. if (this.popupObj != lastPopupNode) {
  14899. var me = this;
  14900. if (!me.popup) {
  14901. me.popup = new Popup(me.frame, me.constants.tooltip);
  14902. }
  14903. // adjust a small offset such that the mouse cursor is located in the
  14904. // bottom left location of the popup, and you can easily move over the
  14905. // popup area
  14906. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  14907. me.popup.setText(me.popupObj.getTitle());
  14908. me.popup.show();
  14909. }
  14910. }
  14911. else {
  14912. if (this.popup) {
  14913. this.popup.hide();
  14914. }
  14915. }
  14916. };
  14917. /**
  14918. * Check if the popup must be hided, which is the case when the mouse is no
  14919. * longer hovering on the object
  14920. * @param {{x:Number, y:Number}} pointer
  14921. * @private
  14922. */
  14923. Graph.prototype._checkHidePopup = function (pointer) {
  14924. if (!this.popupObj || !this._getNodeAt(pointer) ) {
  14925. this.popupObj = undefined;
  14926. if (this.popup) {
  14927. this.popup.hide();
  14928. }
  14929. }
  14930. };
  14931. /**
  14932. * Set a new size for the graph
  14933. * @param {string} width Width in pixels or percentage (for example '800px'
  14934. * or '50%')
  14935. * @param {string} height Height in pixels or percentage (for example '400px'
  14936. * or '30%')
  14937. */
  14938. Graph.prototype.setSize = function(width, height) {
  14939. this.frame.style.width = width;
  14940. this.frame.style.height = height;
  14941. this.frame.canvas.style.width = '100%';
  14942. this.frame.canvas.style.height = '100%';
  14943. this.frame.canvas.width = this.frame.canvas.clientWidth;
  14944. this.frame.canvas.height = this.frame.canvas.clientHeight;
  14945. if (this.manipulationDiv !== undefined) {
  14946. this.manipulationDiv.style.width = this.frame.canvas.clientWidth + "px";
  14947. }
  14948. if (this.navigationDivs !== undefined) {
  14949. if (this.navigationDivs['wrapper'] !== undefined) {
  14950. this.navigationDivs['wrapper'].style.width = this.frame.canvas.clientWidth + "px";
  14951. this.navigationDivs['wrapper'].style.height = this.frame.canvas.clientHeight + "px";
  14952. }
  14953. }
  14954. this.emit('resize', {width:this.frame.canvas.width,height:this.frame.canvas.height});
  14955. };
  14956. /**
  14957. * Set a data set with nodes for the graph
  14958. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  14959. * @private
  14960. */
  14961. Graph.prototype._setNodes = function(nodes) {
  14962. var oldNodesData = this.nodesData;
  14963. if (nodes instanceof DataSet || nodes instanceof DataView) {
  14964. this.nodesData = nodes;
  14965. }
  14966. else if (nodes instanceof Array) {
  14967. this.nodesData = new DataSet();
  14968. this.nodesData.add(nodes);
  14969. }
  14970. else if (!nodes) {
  14971. this.nodesData = new DataSet();
  14972. }
  14973. else {
  14974. throw new TypeError('Array or DataSet expected');
  14975. }
  14976. if (oldNodesData) {
  14977. // unsubscribe from old dataset
  14978. util.forEach(this.nodesListeners, function (callback, event) {
  14979. oldNodesData.off(event, callback);
  14980. });
  14981. }
  14982. // remove drawn nodes
  14983. this.nodes = {};
  14984. if (this.nodesData) {
  14985. // subscribe to new dataset
  14986. var me = this;
  14987. util.forEach(this.nodesListeners, function (callback, event) {
  14988. me.nodesData.on(event, callback);
  14989. });
  14990. // draw all new nodes
  14991. var ids = this.nodesData.getIds();
  14992. this._addNodes(ids);
  14993. }
  14994. this._updateSelection();
  14995. };
  14996. /**
  14997. * Add nodes
  14998. * @param {Number[] | String[]} ids
  14999. * @private
  15000. */
  15001. Graph.prototype._addNodes = function(ids) {
  15002. var id;
  15003. for (var i = 0, len = ids.length; i < len; i++) {
  15004. id = ids[i];
  15005. var data = this.nodesData.get(id);
  15006. var node = new Node(data, this.images, this.groups, this.constants);
  15007. this.nodes[id] = node; // note: this may replace an existing node
  15008. if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
  15009. var radius = 10 * 0.1*ids.length;
  15010. var angle = 2 * Math.PI * Math.random();
  15011. if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
  15012. if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
  15013. }
  15014. this.moving = true;
  15015. }
  15016. this._updateNodeIndexList();
  15017. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15018. this._resetLevels();
  15019. this._setupHierarchicalLayout();
  15020. }
  15021. this._updateCalculationNodes();
  15022. this._reconnectEdges();
  15023. this._updateValueRange(this.nodes);
  15024. this.updateLabels();
  15025. };
  15026. /**
  15027. * Update existing nodes, or create them when not yet existing
  15028. * @param {Number[] | String[]} ids
  15029. * @private
  15030. */
  15031. Graph.prototype._updateNodes = function(ids) {
  15032. var nodes = this.nodes,
  15033. nodesData = this.nodesData;
  15034. for (var i = 0, len = ids.length; i < len; i++) {
  15035. var id = ids[i];
  15036. var node = nodes[id];
  15037. var data = nodesData.get(id);
  15038. if (node) {
  15039. // update node
  15040. node.setProperties(data, this.constants);
  15041. }
  15042. else {
  15043. // create node
  15044. node = new Node(properties, this.images, this.groups, this.constants);
  15045. nodes[id] = node;
  15046. }
  15047. }
  15048. this.moving = true;
  15049. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15050. this._resetLevels();
  15051. this._setupHierarchicalLayout();
  15052. }
  15053. this._updateNodeIndexList();
  15054. this._reconnectEdges();
  15055. this._updateValueRange(nodes);
  15056. };
  15057. /**
  15058. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  15059. * @param {Number[] | String[]} ids
  15060. * @private
  15061. */
  15062. Graph.prototype._removeNodes = function(ids) {
  15063. var nodes = this.nodes;
  15064. for (var i = 0, len = ids.length; i < len; i++) {
  15065. var id = ids[i];
  15066. delete nodes[id];
  15067. }
  15068. this._updateNodeIndexList();
  15069. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15070. this._resetLevels();
  15071. this._setupHierarchicalLayout();
  15072. }
  15073. this._updateCalculationNodes();
  15074. this._reconnectEdges();
  15075. this._updateSelection();
  15076. this._updateValueRange(nodes);
  15077. };
  15078. /**
  15079. * Load edges by reading the data table
  15080. * @param {Array | DataSet | DataView} edges The data containing the edges.
  15081. * @private
  15082. * @private
  15083. */
  15084. Graph.prototype._setEdges = function(edges) {
  15085. var oldEdgesData = this.edgesData;
  15086. if (edges instanceof DataSet || edges instanceof DataView) {
  15087. this.edgesData = edges;
  15088. }
  15089. else if (edges instanceof Array) {
  15090. this.edgesData = new DataSet();
  15091. this.edgesData.add(edges);
  15092. }
  15093. else if (!edges) {
  15094. this.edgesData = new DataSet();
  15095. }
  15096. else {
  15097. throw new TypeError('Array or DataSet expected');
  15098. }
  15099. if (oldEdgesData) {
  15100. // unsubscribe from old dataset
  15101. util.forEach(this.edgesListeners, function (callback, event) {
  15102. oldEdgesData.off(event, callback);
  15103. });
  15104. }
  15105. // remove drawn edges
  15106. this.edges = {};
  15107. if (this.edgesData) {
  15108. // subscribe to new dataset
  15109. var me = this;
  15110. util.forEach(this.edgesListeners, function (callback, event) {
  15111. me.edgesData.on(event, callback);
  15112. });
  15113. // draw all new nodes
  15114. var ids = this.edgesData.getIds();
  15115. this._addEdges(ids);
  15116. }
  15117. this._reconnectEdges();
  15118. };
  15119. /**
  15120. * Add edges
  15121. * @param {Number[] | String[]} ids
  15122. * @private
  15123. */
  15124. Graph.prototype._addEdges = function (ids) {
  15125. var edges = this.edges,
  15126. edgesData = this.edgesData;
  15127. for (var i = 0, len = ids.length; i < len; i++) {
  15128. var id = ids[i];
  15129. var oldEdge = edges[id];
  15130. if (oldEdge) {
  15131. oldEdge.disconnect();
  15132. }
  15133. var data = edgesData.get(id, {"showInternalIds" : true});
  15134. edges[id] = new Edge(data, this, this.constants);
  15135. }
  15136. this.moving = true;
  15137. this._updateValueRange(edges);
  15138. this._createBezierNodes();
  15139. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15140. this._resetLevels();
  15141. this._setupHierarchicalLayout();
  15142. }
  15143. this._updateCalculationNodes();
  15144. };
  15145. /**
  15146. * Update existing edges, or create them when not yet existing
  15147. * @param {Number[] | String[]} ids
  15148. * @private
  15149. */
  15150. Graph.prototype._updateEdges = function (ids) {
  15151. var edges = this.edges,
  15152. edgesData = this.edgesData;
  15153. for (var i = 0, len = ids.length; i < len; i++) {
  15154. var id = ids[i];
  15155. var data = edgesData.get(id);
  15156. var edge = edges[id];
  15157. if (edge) {
  15158. // update edge
  15159. edge.disconnect();
  15160. edge.setProperties(data, this.constants);
  15161. edge.connect();
  15162. }
  15163. else {
  15164. // create edge
  15165. edge = new Edge(data, this, this.constants);
  15166. this.edges[id] = edge;
  15167. }
  15168. }
  15169. this._createBezierNodes();
  15170. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15171. this._resetLevels();
  15172. this._setupHierarchicalLayout();
  15173. }
  15174. this.moving = true;
  15175. this._updateValueRange(edges);
  15176. };
  15177. /**
  15178. * Remove existing edges. Non existing ids will be ignored
  15179. * @param {Number[] | String[]} ids
  15180. * @private
  15181. */
  15182. Graph.prototype._removeEdges = function (ids) {
  15183. var edges = this.edges;
  15184. for (var i = 0, len = ids.length; i < len; i++) {
  15185. var id = ids[i];
  15186. var edge = edges[id];
  15187. if (edge) {
  15188. if (edge.via != null) {
  15189. delete this.sectors['support']['nodes'][edge.via.id];
  15190. }
  15191. edge.disconnect();
  15192. delete edges[id];
  15193. }
  15194. }
  15195. this.moving = true;
  15196. this._updateValueRange(edges);
  15197. if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
  15198. this._resetLevels();
  15199. this._setupHierarchicalLayout();
  15200. }
  15201. this._updateCalculationNodes();
  15202. };
  15203. /**
  15204. * Reconnect all edges
  15205. * @private
  15206. */
  15207. Graph.prototype._reconnectEdges = function() {
  15208. var id,
  15209. nodes = this.nodes,
  15210. edges = this.edges;
  15211. for (id in nodes) {
  15212. if (nodes.hasOwnProperty(id)) {
  15213. nodes[id].edges = [];
  15214. }
  15215. }
  15216. for (id in edges) {
  15217. if (edges.hasOwnProperty(id)) {
  15218. var edge = edges[id];
  15219. edge.from = null;
  15220. edge.to = null;
  15221. edge.connect();
  15222. }
  15223. }
  15224. };
  15225. /**
  15226. * Update the values of all object in the given array according to the current
  15227. * value range of the objects in the array.
  15228. * @param {Object} obj An object containing a set of Edges or Nodes
  15229. * The objects must have a method getValue() and
  15230. * setValueRange(min, max).
  15231. * @private
  15232. */
  15233. Graph.prototype._updateValueRange = function(obj) {
  15234. var id;
  15235. // determine the range of the objects
  15236. var valueMin = undefined;
  15237. var valueMax = undefined;
  15238. for (id in obj) {
  15239. if (obj.hasOwnProperty(id)) {
  15240. var value = obj[id].getValue();
  15241. if (value !== undefined) {
  15242. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  15243. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  15244. }
  15245. }
  15246. }
  15247. // adjust the range of all objects
  15248. if (valueMin !== undefined && valueMax !== undefined) {
  15249. for (id in obj) {
  15250. if (obj.hasOwnProperty(id)) {
  15251. obj[id].setValueRange(valueMin, valueMax);
  15252. }
  15253. }
  15254. }
  15255. };
  15256. /**
  15257. * Redraw the graph with the current data
  15258. * chart will be resized too.
  15259. */
  15260. Graph.prototype.redraw = function() {
  15261. this.setSize(this.width, this.height);
  15262. this._redraw();
  15263. };
  15264. /**
  15265. * Redraw the graph with the current data
  15266. * @private
  15267. */
  15268. Graph.prototype._redraw = function() {
  15269. var ctx = this.frame.canvas.getContext('2d');
  15270. // clear the canvas
  15271. var w = this.frame.canvas.width;
  15272. var h = this.frame.canvas.height;
  15273. ctx.clearRect(0, 0, w, h);
  15274. // set scaling and translation
  15275. ctx.save();
  15276. ctx.translate(this.translation.x, this.translation.y);
  15277. ctx.scale(this.scale, this.scale);
  15278. this.canvasTopLeft = {
  15279. "x": this._XconvertDOMtoCanvas(0),
  15280. "y": this._YconvertDOMtoCanvas(0)
  15281. };
  15282. this.canvasBottomRight = {
  15283. "x": this._XconvertDOMtoCanvas(this.frame.canvas.clientWidth),
  15284. "y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
  15285. };
  15286. this._doInAllSectors("_drawAllSectorNodes",ctx);
  15287. this._doInAllSectors("_drawEdges",ctx);
  15288. this._doInAllSectors("_drawNodes",ctx,false);
  15289. // this._doInSupportSector("_drawNodes",ctx,true);
  15290. // this._drawTree(ctx,"#F00F0F");
  15291. // restore original scaling and translation
  15292. ctx.restore();
  15293. };
  15294. /**
  15295. * Set the translation of the graph
  15296. * @param {Number} offsetX Horizontal offset
  15297. * @param {Number} offsetY Vertical offset
  15298. * @private
  15299. */
  15300. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  15301. if (this.translation === undefined) {
  15302. this.translation = {
  15303. x: 0,
  15304. y: 0
  15305. };
  15306. }
  15307. if (offsetX !== undefined) {
  15308. this.translation.x = offsetX;
  15309. }
  15310. if (offsetY !== undefined) {
  15311. this.translation.y = offsetY;
  15312. }
  15313. this.emit('viewChanged');
  15314. };
  15315. /**
  15316. * Get the translation of the graph
  15317. * @return {Object} translation An object with parameters x and y, both a number
  15318. * @private
  15319. */
  15320. Graph.prototype._getTranslation = function() {
  15321. return {
  15322. x: this.translation.x,
  15323. y: this.translation.y
  15324. };
  15325. };
  15326. /**
  15327. * Scale the graph
  15328. * @param {Number} scale Scaling factor 1.0 is unscaled
  15329. * @private
  15330. */
  15331. Graph.prototype._setScale = function(scale) {
  15332. this.scale = scale;
  15333. };
  15334. /**
  15335. * Get the current scale of the graph
  15336. * @return {Number} scale Scaling factor 1.0 is unscaled
  15337. * @private
  15338. */
  15339. Graph.prototype._getScale = function() {
  15340. return this.scale;
  15341. };
  15342. /**
  15343. * Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to
  15344. * the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  15345. * @param {number} x
  15346. * @returns {number}
  15347. * @private
  15348. */
  15349. Graph.prototype._XconvertDOMtoCanvas = function(x) {
  15350. return (x - this.translation.x) / this.scale;
  15351. };
  15352. /**
  15353. * Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  15354. * the X coordinate in DOM-space (coordinate point in browser relative to the container div)
  15355. * @param {number} x
  15356. * @returns {number}
  15357. * @private
  15358. */
  15359. Graph.prototype._XconvertCanvasToDOM = function(x) {
  15360. return x * this.scale + this.translation.x;
  15361. };
  15362. /**
  15363. * Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to
  15364. * the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon)
  15365. * @param {number} y
  15366. * @returns {number}
  15367. * @private
  15368. */
  15369. Graph.prototype._YconvertDOMtoCanvas = function(y) {
  15370. return (y - this.translation.y) / this.scale;
  15371. };
  15372. /**
  15373. * Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to
  15374. * the Y coordinate in DOM-space (coordinate point in browser relative to the container div)
  15375. * @param {number} y
  15376. * @returns {number}
  15377. * @private
  15378. */
  15379. Graph.prototype._YconvertCanvasToDOM = function(y) {
  15380. return y * this.scale + this.translation.y ;
  15381. };
  15382. /**
  15383. *
  15384. * @param {object} pos = {x: number, y: number}
  15385. * @returns {{x: number, y: number}}
  15386. * @constructor
  15387. */
  15388. Graph.prototype.canvasToDOM = function(pos) {
  15389. return {x:this._XconvertCanvasToDOM(pos.x),y:this._YconvertCanvasToDOM(pos.y)};
  15390. }
  15391. /**
  15392. *
  15393. * @param {object} pos = {x: number, y: number}
  15394. * @returns {{x: number, y: number}}
  15395. * @constructor
  15396. */
  15397. Graph.prototype.DOMtoCanvas = function(pos) {
  15398. return {x:this._XconvertDOMtoCanvas(pos.x),y:this._YconvertDOMtoCanvas(pos.y)};
  15399. }
  15400. /**
  15401. * Redraw all nodes
  15402. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15403. * @param {CanvasRenderingContext2D} ctx
  15404. * @param {Boolean} [alwaysShow]
  15405. * @private
  15406. */
  15407. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  15408. if (alwaysShow === undefined) {
  15409. alwaysShow = false;
  15410. }
  15411. // first draw the unselected nodes
  15412. var nodes = this.nodes;
  15413. var selected = [];
  15414. for (var id in nodes) {
  15415. if (nodes.hasOwnProperty(id)) {
  15416. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  15417. if (nodes[id].isSelected()) {
  15418. selected.push(id);
  15419. }
  15420. else {
  15421. if (nodes[id].inArea() || alwaysShow) {
  15422. nodes[id].draw(ctx);
  15423. }
  15424. }
  15425. }
  15426. }
  15427. // draw the selected nodes on top
  15428. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  15429. if (nodes[selected[s]].inArea() || alwaysShow) {
  15430. nodes[selected[s]].draw(ctx);
  15431. }
  15432. }
  15433. };
  15434. /**
  15435. * Redraw all edges
  15436. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15437. * @param {CanvasRenderingContext2D} ctx
  15438. * @private
  15439. */
  15440. Graph.prototype._drawEdges = function(ctx) {
  15441. var edges = this.edges;
  15442. for (var id in edges) {
  15443. if (edges.hasOwnProperty(id)) {
  15444. var edge = edges[id];
  15445. edge.setScale(this.scale);
  15446. if (edge.connected) {
  15447. edges[id].draw(ctx);
  15448. }
  15449. }
  15450. }
  15451. };
  15452. /**
  15453. * Find a stable position for all nodes
  15454. * @private
  15455. */
  15456. Graph.prototype._stabilize = function() {
  15457. if (this.constants.freezeForStabilization == true) {
  15458. this._freezeDefinedNodes();
  15459. }
  15460. // find stable position
  15461. var count = 0;
  15462. while (this.moving && count < this.constants.stabilizationIterations) {
  15463. this._physicsTick();
  15464. count++;
  15465. }
  15466. this.zoomExtent(false,true);
  15467. if (this.constants.freezeForStabilization == true) {
  15468. this._restoreFrozenNodes();
  15469. }
  15470. this.emit("stabilized",{iterations:count});
  15471. };
  15472. /**
  15473. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  15474. * because only the supportnodes for the smoothCurves have to settle.
  15475. *
  15476. * @private
  15477. */
  15478. Graph.prototype._freezeDefinedNodes = function() {
  15479. var nodes = this.nodes;
  15480. for (var id in nodes) {
  15481. if (nodes.hasOwnProperty(id)) {
  15482. if (nodes[id].x != null && nodes[id].y != null) {
  15483. nodes[id].fixedData.x = nodes[id].xFixed;
  15484. nodes[id].fixedData.y = nodes[id].yFixed;
  15485. nodes[id].xFixed = true;
  15486. nodes[id].yFixed = true;
  15487. }
  15488. }
  15489. }
  15490. };
  15491. /**
  15492. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  15493. *
  15494. * @private
  15495. */
  15496. Graph.prototype._restoreFrozenNodes = function() {
  15497. var nodes = this.nodes;
  15498. for (var id in nodes) {
  15499. if (nodes.hasOwnProperty(id)) {
  15500. if (nodes[id].fixedData.x != null) {
  15501. nodes[id].xFixed = nodes[id].fixedData.x;
  15502. nodes[id].yFixed = nodes[id].fixedData.y;
  15503. }
  15504. }
  15505. }
  15506. };
  15507. /**
  15508. * Check if any of the nodes is still moving
  15509. * @param {number} vmin the minimum velocity considered as 'moving'
  15510. * @return {boolean} true if moving, false if non of the nodes is moving
  15511. * @private
  15512. */
  15513. Graph.prototype._isMoving = function(vmin) {
  15514. var nodes = this.nodes;
  15515. for (var id in nodes) {
  15516. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15517. return true;
  15518. }
  15519. }
  15520. return false;
  15521. };
  15522. /**
  15523. * /**
  15524. * Perform one discrete step for all nodes
  15525. *
  15526. * @private
  15527. */
  15528. Graph.prototype._discreteStepNodes = function() {
  15529. var interval = this.physicsDiscreteStepsize;
  15530. var nodes = this.nodes;
  15531. var nodeId;
  15532. var nodesPresent = false;
  15533. if (this.constants.maxVelocity > 0) {
  15534. for (nodeId in nodes) {
  15535. if (nodes.hasOwnProperty(nodeId)) {
  15536. nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
  15537. nodesPresent = true;
  15538. }
  15539. }
  15540. }
  15541. else {
  15542. for (nodeId in nodes) {
  15543. if (nodes.hasOwnProperty(nodeId)) {
  15544. nodes[nodeId].discreteStep(interval);
  15545. nodesPresent = true;
  15546. }
  15547. }
  15548. }
  15549. if (nodesPresent == true) {
  15550. var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
  15551. if (vminCorrected > 0.5*this.constants.maxVelocity) {
  15552. this.moving = true;
  15553. }
  15554. else {
  15555. this.moving = this._isMoving(vminCorrected);
  15556. }
  15557. }
  15558. };
  15559. /**
  15560. * A single simulation step (or "tick") in the physics simulation
  15561. *
  15562. * @private
  15563. */
  15564. Graph.prototype._physicsTick = function() {
  15565. if (!this.freezeSimulation) {
  15566. if (this.moving) {
  15567. this._doInAllActiveSectors("_initializeForceCalculation");
  15568. this._doInAllActiveSectors("_discreteStepNodes");
  15569. if (this.constants.smoothCurves) {
  15570. this._doInSupportSector("_discreteStepNodes");
  15571. }
  15572. this._findCenter(this._getRange())
  15573. }
  15574. }
  15575. };
  15576. /**
  15577. * This function runs one step of the animation. It calls an x amount of physics ticks and one render tick.
  15578. * It reschedules itself at the beginning of the function
  15579. *
  15580. * @private
  15581. */
  15582. Graph.prototype._animationStep = function() {
  15583. // reset the timer so a new scheduled animation step can be set
  15584. this.timer = undefined;
  15585. // handle the keyboad movement
  15586. this._handleNavigation();
  15587. // this schedules a new animation step
  15588. this.start();
  15589. // start the physics simulation
  15590. var calculationTime = Date.now();
  15591. var maxSteps = 1;
  15592. this._physicsTick();
  15593. var timeRequired = Date.now() - calculationTime;
  15594. while (timeRequired < (this.renderTimestep - this.renderTime) && maxSteps < this.maxPhysicsTicksPerRender) {
  15595. this._physicsTick();
  15596. timeRequired = Date.now() - calculationTime;
  15597. maxSteps++;
  15598. }
  15599. // start the rendering process
  15600. var renderTime = Date.now();
  15601. this._redraw();
  15602. this.renderTime = Date.now() - renderTime;
  15603. };
  15604. if (typeof window !== 'undefined') {
  15605. window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  15606. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  15607. }
  15608. /**
  15609. * Schedule a animation step with the refreshrate interval.
  15610. */
  15611. Graph.prototype.start = function() {
  15612. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  15613. if (!this.timer) {
  15614. var ua = navigator.userAgent.toLowerCase();
  15615. var requiresTimeout = false;
  15616. if (ua.indexOf('msie 9.0') != -1) { // IE 9
  15617. requiresTimeout = true;
  15618. }
  15619. else if (ua.indexOf('safari') != -1) { // safari
  15620. if (ua.indexOf('chrome') <= -1) {
  15621. requiresTimeout = true;
  15622. }
  15623. }
  15624. if (requiresTimeout == true) {
  15625. this.timer = window.setTimeout(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15626. }
  15627. else{
  15628. this.timer = window.requestAnimationFrame(this._animationStep.bind(this), this.renderTimestep); // wait this.renderTimeStep milliseconds and perform the animation step function
  15629. }
  15630. }
  15631. }
  15632. else {
  15633. this._redraw();
  15634. }
  15635. };
  15636. /**
  15637. * Move the graph according to the keyboard presses.
  15638. *
  15639. * @private
  15640. */
  15641. Graph.prototype._handleNavigation = function() {
  15642. if (this.xIncrement != 0 || this.yIncrement != 0) {
  15643. var translation = this._getTranslation();
  15644. this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
  15645. }
  15646. if (this.zoomIncrement != 0) {
  15647. var center = {
  15648. x: this.frame.canvas.clientWidth / 2,
  15649. y: this.frame.canvas.clientHeight / 2
  15650. };
  15651. this._zoom(this.scale*(1 + this.zoomIncrement), center);
  15652. }
  15653. };
  15654. /**
  15655. * Freeze the _animationStep
  15656. */
  15657. Graph.prototype.toggleFreeze = function() {
  15658. if (this.freezeSimulation == false) {
  15659. this.freezeSimulation = true;
  15660. }
  15661. else {
  15662. this.freezeSimulation = false;
  15663. this.start();
  15664. }
  15665. };
  15666. /**
  15667. * This function cleans the support nodes if they are not needed and adds them when they are.
  15668. *
  15669. * @param {boolean} [disableStart]
  15670. * @private
  15671. */
  15672. Graph.prototype._configureSmoothCurves = function(disableStart) {
  15673. if (disableStart === undefined) {
  15674. disableStart = true;
  15675. }
  15676. if (this.constants.smoothCurves == true) {
  15677. this._createBezierNodes();
  15678. }
  15679. else {
  15680. // delete the support nodes
  15681. this.sectors['support']['nodes'] = {};
  15682. for (var edgeId in this.edges) {
  15683. if (this.edges.hasOwnProperty(edgeId)) {
  15684. this.edges[edgeId].smooth = false;
  15685. this.edges[edgeId].via = null;
  15686. }
  15687. }
  15688. }
  15689. this._updateCalculationNodes();
  15690. if (!disableStart) {
  15691. this.moving = true;
  15692. this.start();
  15693. }
  15694. };
  15695. /**
  15696. * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
  15697. * are used for the force calculation.
  15698. *
  15699. * @private
  15700. */
  15701. Graph.prototype._createBezierNodes = function() {
  15702. if (this.constants.smoothCurves == true) {
  15703. for (var edgeId in this.edges) {
  15704. if (this.edges.hasOwnProperty(edgeId)) {
  15705. var edge = this.edges[edgeId];
  15706. if (edge.via == null) {
  15707. edge.smooth = true;
  15708. var nodeId = "edgeId:".concat(edge.id);
  15709. this.sectors['support']['nodes'][nodeId] = new Node(
  15710. {id:nodeId,
  15711. mass:1,
  15712. shape:'circle',
  15713. image:"",
  15714. internalMultiplier:1
  15715. },{},{},this.constants);
  15716. edge.via = this.sectors['support']['nodes'][nodeId];
  15717. edge.via.parentEdgeId = edge.id;
  15718. edge.positionBezierNode();
  15719. }
  15720. }
  15721. }
  15722. }
  15723. };
  15724. /**
  15725. * load the functions that load the mixins into the prototype.
  15726. *
  15727. * @private
  15728. */
  15729. Graph.prototype._initializeMixinLoaders = function () {
  15730. for (var mixinFunction in graphMixinLoaders) {
  15731. if (graphMixinLoaders.hasOwnProperty(mixinFunction)) {
  15732. Graph.prototype[mixinFunction] = graphMixinLoaders[mixinFunction];
  15733. }
  15734. }
  15735. };
  15736. /**
  15737. * Load the XY positions of the nodes into the dataset.
  15738. */
  15739. Graph.prototype.storePosition = function() {
  15740. var dataArray = [];
  15741. for (var nodeId in this.nodes) {
  15742. if (this.nodes.hasOwnProperty(nodeId)) {
  15743. var node = this.nodes[nodeId];
  15744. var allowedToMoveX = !this.nodes.xFixed;
  15745. var allowedToMoveY = !this.nodes.yFixed;
  15746. if (this.nodesData.data[nodeId].x != Math.round(node.x) || this.nodesData.data[nodeId].y != Math.round(node.y)) {
  15747. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  15748. }
  15749. }
  15750. }
  15751. this.nodesData.update(dataArray);
  15752. };
  15753. /**
  15754. * Center a node in view.
  15755. *
  15756. * @param {Number} nodeId
  15757. * @param {Number} [zoomLevel]
  15758. */
  15759. Graph.prototype.focusOnNode = function (nodeId, zoomLevel) {
  15760. if (this.nodes.hasOwnProperty(nodeId)) {
  15761. if (zoomLevel === undefined) {
  15762. zoomLevel = this._getScale();
  15763. }
  15764. var nodePosition= {x: this.nodes[nodeId].x, y: this.nodes[nodeId].y};
  15765. var requiredScale = zoomLevel;
  15766. this._setScale(requiredScale);
  15767. var canvasCenter = this.DOMtoCanvas({x:0.5 * this.frame.canvas.width,y:0.5 * this.frame.canvas.height});
  15768. var translation = this._getTranslation();
  15769. var distanceFromCenter = {x:canvasCenter.x - nodePosition.x,
  15770. y:canvasCenter.y - nodePosition.y};
  15771. this._setTranslation(translation.x + requiredScale * distanceFromCenter.x,
  15772. translation.y + requiredScale * distanceFromCenter.y);
  15773. this.redraw();
  15774. }
  15775. else {
  15776. console.log("This nodeId cannot be found.")
  15777. }
  15778. };
  15779. /**
  15780. * @constructor Graph3d
  15781. * The Graph is a visualization Graphs on a time line
  15782. *
  15783. * Graph is developed in javascript as a Google Visualization Chart.
  15784. *
  15785. * @param {Element} container The DOM element in which the Graph will
  15786. * be created. Normally a div element.
  15787. * @param {DataSet | DataView | Array} [data]
  15788. * @param {Object} [options]
  15789. */
  15790. function Graph3d(container, data, options) {
  15791. // create variables and set default values
  15792. this.containerElement = container;
  15793. this.width = '400px';
  15794. this.height = '400px';
  15795. this.margin = 10; // px
  15796. this.defaultXCenter = '55%';
  15797. this.defaultYCenter = '50%';
  15798. this.xLabel = 'x';
  15799. this.yLabel = 'y';
  15800. this.zLabel = 'z';
  15801. this.filterLabel = 'time';
  15802. this.legendLabel = 'value';
  15803. this.style = Graph3d.STYLE.DOT;
  15804. this.showPerspective = true;
  15805. this.showGrid = true;
  15806. this.keepAspectRatio = true;
  15807. this.showShadow = false;
  15808. this.showGrayBottom = false; // TODO: this does not work correctly
  15809. this.showTooltip = false;
  15810. this.verticalRatio = 0.5; // 0.1 to 1.0, where 1.0 results in a 'cube'
  15811. this.animationInterval = 1000; // milliseconds
  15812. this.animationPreload = false;
  15813. this.camera = new Graph3d.Camera();
  15814. this.eye = new Point3d(0, 0, -1); // TODO: set eye.z about 3/4 of the width of the window?
  15815. this.dataTable = null; // The original data table
  15816. this.dataPoints = null; // The table with point objects
  15817. // the column indexes
  15818. this.colX = undefined;
  15819. this.colY = undefined;
  15820. this.colZ = undefined;
  15821. this.colValue = undefined;
  15822. this.colFilter = undefined;
  15823. this.xMin = 0;
  15824. this.xStep = undefined; // auto by default
  15825. this.xMax = 1;
  15826. this.yMin = 0;
  15827. this.yStep = undefined; // auto by default
  15828. this.yMax = 1;
  15829. this.zMin = 0;
  15830. this.zStep = undefined; // auto by default
  15831. this.zMax = 1;
  15832. this.valueMin = 0;
  15833. this.valueMax = 1;
  15834. this.xBarWidth = 1;
  15835. this.yBarWidth = 1;
  15836. // TODO: customize axis range
  15837. // constants
  15838. this.colorAxis = '#4D4D4D';
  15839. this.colorGrid = '#D3D3D3';
  15840. this.colorDot = '#7DC1FF';
  15841. this.colorDotBorder = '#3267D2';
  15842. // create a frame and canvas
  15843. this.create();
  15844. // apply options (also when undefined)
  15845. this.setOptions(options);
  15846. // apply data
  15847. if (data) {
  15848. this.setData(data);
  15849. }
  15850. }
  15851. // Extend Graph with an Emitter mixin
  15852. Emitter(Graph3d.prototype);
  15853. /**
  15854. * @class Camera
  15855. * The camera is mounted on a (virtual) camera arm. The camera arm can rotate
  15856. * The camera is always looking in the direction of the origin of the arm.
  15857. * This way, the camera always rotates around one fixed point, the location
  15858. * of the camera arm.
  15859. *
  15860. * Documentation:
  15861. * http://en.wikipedia.org/wiki/3D_projection
  15862. */
  15863. Graph3d.Camera = function () {
  15864. this.armLocation = new Point3d();
  15865. this.armRotation = {};
  15866. this.armRotation.horizontal = 0;
  15867. this.armRotation.vertical = 0;
  15868. this.armLength = 1.7;
  15869. this.cameraLocation = new Point3d();
  15870. this.cameraRotation = new Point3d(0.5*Math.PI, 0, 0);
  15871. this.calculateCameraOrientation();
  15872. };
  15873. /**
  15874. * Set the location (origin) of the arm
  15875. * @param {Number} x Normalized value of x
  15876. * @param {Number} y Normalized value of y
  15877. * @param {Number} z Normalized value of z
  15878. */
  15879. Graph3d.Camera.prototype.setArmLocation = function(x, y, z) {
  15880. this.armLocation.x = x;
  15881. this.armLocation.y = y;
  15882. this.armLocation.z = z;
  15883. this.calculateCameraOrientation();
  15884. };
  15885. /**
  15886. * Set the rotation of the camera arm
  15887. * @param {Number} horizontal The horizontal rotation, between 0 and 2*PI.
  15888. * Optional, can be left undefined.
  15889. * @param {Number} vertical The vertical rotation, between 0 and 0.5*PI
  15890. * if vertical=0.5*PI, the graph is shown from the
  15891. * top. Optional, can be left undefined.
  15892. */
  15893. Graph3d.Camera.prototype.setArmRotation = function(horizontal, vertical) {
  15894. if (horizontal !== undefined) {
  15895. this.armRotation.horizontal = horizontal;
  15896. }
  15897. if (vertical !== undefined) {
  15898. this.armRotation.vertical = vertical;
  15899. if (this.armRotation.vertical < 0) this.armRotation.vertical = 0;
  15900. if (this.armRotation.vertical > 0.5*Math.PI) this.armRotation.vertical = 0.5*Math.PI;
  15901. }
  15902. if (horizontal !== undefined || vertical !== undefined) {
  15903. this.calculateCameraOrientation();
  15904. }
  15905. };
  15906. /**
  15907. * Retrieve the current arm rotation
  15908. * @return {object} An object with parameters horizontal and vertical
  15909. */
  15910. Graph3d.Camera.prototype.getArmRotation = function() {
  15911. var rot = {};
  15912. rot.horizontal = this.armRotation.horizontal;
  15913. rot.vertical = this.armRotation.vertical;
  15914. return rot;
  15915. };
  15916. /**
  15917. * Set the (normalized) length of the camera arm.
  15918. * @param {Number} length A length between 0.71 and 5.0
  15919. */
  15920. Graph3d.Camera.prototype.setArmLength = function(length) {
  15921. if (length === undefined)
  15922. return;
  15923. this.armLength = length;
  15924. // Radius must be larger than the corner of the graph,
  15925. // which has a distance of sqrt(0.5^2+0.5^2) = 0.71 from the center of the
  15926. // graph
  15927. if (this.armLength < 0.71) this.armLength = 0.71;
  15928. if (this.armLength > 5.0) this.armLength = 5.0;
  15929. this.calculateCameraOrientation();
  15930. };
  15931. /**
  15932. * Retrieve the arm length
  15933. * @return {Number} length
  15934. */
  15935. Graph3d.Camera.prototype.getArmLength = function() {
  15936. return this.armLength;
  15937. };
  15938. /**
  15939. * Retrieve the camera location
  15940. * @return {Point3d} cameraLocation
  15941. */
  15942. Graph3d.Camera.prototype.getCameraLocation = function() {
  15943. return this.cameraLocation;
  15944. };
  15945. /**
  15946. * Retrieve the camera rotation
  15947. * @return {Point3d} cameraRotation
  15948. */
  15949. Graph3d.Camera.prototype.getCameraRotation = function() {
  15950. return this.cameraRotation;
  15951. };
  15952. /**
  15953. * Calculate the location and rotation of the camera based on the
  15954. * position and orientation of the camera arm
  15955. */
  15956. Graph3d.Camera.prototype.calculateCameraOrientation = function() {
  15957. // calculate location of the camera
  15958. this.cameraLocation.x = this.armLocation.x - this.armLength * Math.sin(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  15959. this.cameraLocation.y = this.armLocation.y - this.armLength * Math.cos(this.armRotation.horizontal) * Math.cos(this.armRotation.vertical);
  15960. this.cameraLocation.z = this.armLocation.z + this.armLength * Math.sin(this.armRotation.vertical);
  15961. // calculate rotation of the camera
  15962. this.cameraRotation.x = Math.PI/2 - this.armRotation.vertical;
  15963. this.cameraRotation.y = 0;
  15964. this.cameraRotation.z = -this.armRotation.horizontal;
  15965. };
  15966. /**
  15967. * Calculate the scaling values, dependent on the range in x, y, and z direction
  15968. */
  15969. Graph3d.prototype._setScale = function() {
  15970. this.scale = new Point3d(1 / (this.xMax - this.xMin),
  15971. 1 / (this.yMax - this.yMin),
  15972. 1 / (this.zMax - this.zMin));
  15973. // keep aspect ration between x and y scale if desired
  15974. if (this.keepAspectRatio) {
  15975. if (this.scale.x < this.scale.y) {
  15976. //noinspection JSSuspiciousNameCombination
  15977. this.scale.y = this.scale.x;
  15978. }
  15979. else {
  15980. //noinspection JSSuspiciousNameCombination
  15981. this.scale.x = this.scale.y;
  15982. }
  15983. }
  15984. // scale the vertical axis
  15985. this.scale.z *= this.verticalRatio;
  15986. // TODO: can this be automated? verticalRatio?
  15987. // determine scale for (optional) value
  15988. this.scale.value = 1 / (this.valueMax - this.valueMin);
  15989. // position the camera arm
  15990. var xCenter = (this.xMax + this.xMin) / 2 * this.scale.x;
  15991. var yCenter = (this.yMax + this.yMin) / 2 * this.scale.y;
  15992. var zCenter = (this.zMax + this.zMin) / 2 * this.scale.z;
  15993. this.camera.setArmLocation(xCenter, yCenter, zCenter);
  15994. };
  15995. /**
  15996. * Convert a 3D location to a 2D location on screen
  15997. * http://en.wikipedia.org/wiki/3D_projection
  15998. * @param {Point3d} point3d A 3D point with parameters x, y, z
  15999. * @return {Point2d} point2d A 2D point with parameters x, y
  16000. */
  16001. Graph3d.prototype._convert3Dto2D = function(point3d) {
  16002. var translation = this._convertPointToTranslation(point3d);
  16003. return this._convertTranslationToScreen(translation);
  16004. };
  16005. /**
  16006. * Convert a 3D location its translation seen from the camera
  16007. * http://en.wikipedia.org/wiki/3D_projection
  16008. * @param {Point3d} point3d A 3D point with parameters x, y, z
  16009. * @return {Point3d} translation A 3D point with parameters x, y, z This is
  16010. * the translation of the point, seen from the
  16011. * camera
  16012. */
  16013. Graph3d.prototype._convertPointToTranslation = function(point3d) {
  16014. var ax = point3d.x * this.scale.x,
  16015. ay = point3d.y * this.scale.y,
  16016. az = point3d.z * this.scale.z,
  16017. cx = this.camera.getCameraLocation().x,
  16018. cy = this.camera.getCameraLocation().y,
  16019. cz = this.camera.getCameraLocation().z,
  16020. // calculate angles
  16021. sinTx = Math.sin(this.camera.getCameraRotation().x),
  16022. cosTx = Math.cos(this.camera.getCameraRotation().x),
  16023. sinTy = Math.sin(this.camera.getCameraRotation().y),
  16024. cosTy = Math.cos(this.camera.getCameraRotation().y),
  16025. sinTz = Math.sin(this.camera.getCameraRotation().z),
  16026. cosTz = Math.cos(this.camera.getCameraRotation().z),
  16027. // calculate translation
  16028. dx = cosTy * (sinTz * (ay - cy) + cosTz * (ax - cx)) - sinTy * (az - cz),
  16029. dy = sinTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) + cosTx * (cosTz * (ay - cy) - sinTz * (ax-cx)),
  16030. dz = cosTx * (cosTy * (az - cz) + sinTy * (sinTz * (ay - cy) + cosTz * (ax - cx))) - sinTx * (cosTz * (ay - cy) - sinTz * (ax-cx));
  16031. return new Point3d(dx, dy, dz);
  16032. };
  16033. /**
  16034. * Convert a translation point to a point on the screen
  16035. * @param {Point3d} translation A 3D point with parameters x, y, z This is
  16036. * the translation of the point, seen from the
  16037. * camera
  16038. * @return {Point2d} point2d A 2D point with parameters x, y
  16039. */
  16040. Graph3d.prototype._convertTranslationToScreen = function(translation) {
  16041. var ex = this.eye.x,
  16042. ey = this.eye.y,
  16043. ez = this.eye.z,
  16044. dx = translation.x,
  16045. dy = translation.y,
  16046. dz = translation.z;
  16047. // calculate position on screen from translation
  16048. var bx;
  16049. var by;
  16050. if (this.showPerspective) {
  16051. bx = (dx - ex) * (ez / dz);
  16052. by = (dy - ey) * (ez / dz);
  16053. }
  16054. else {
  16055. bx = dx * -(ez / this.camera.getArmLength());
  16056. by = dy * -(ez / this.camera.getArmLength());
  16057. }
  16058. // shift and scale the point to the center of the screen
  16059. // use the width of the graph to scale both horizontally and vertically.
  16060. return new Point2d(
  16061. this.xcenter + bx * this.frame.canvas.clientWidth,
  16062. this.ycenter - by * this.frame.canvas.clientWidth);
  16063. };
  16064. /**
  16065. * Set the background styling for the graph
  16066. * @param {string | {fill: string, stroke: string, strokeWidth: string}} backgroundColor
  16067. */
  16068. Graph3d.prototype._setBackgroundColor = function(backgroundColor) {
  16069. var fill = 'white';
  16070. var stroke = 'gray';
  16071. var strokeWidth = 1;
  16072. if (typeof(backgroundColor) === 'string') {
  16073. fill = backgroundColor;
  16074. stroke = 'none';
  16075. strokeWidth = 0;
  16076. }
  16077. else if (typeof(backgroundColor) === 'object') {
  16078. if (backgroundColor.fill !== undefined) fill = backgroundColor.fill;
  16079. if (backgroundColor.stroke !== undefined) stroke = backgroundColor.stroke;
  16080. if (backgroundColor.strokeWidth !== undefined) strokeWidth = backgroundColor.strokeWidth;
  16081. }
  16082. else if (backgroundColor === undefined) {
  16083. // use use defaults
  16084. }
  16085. else {
  16086. throw 'Unsupported type of backgroundColor';
  16087. }
  16088. this.frame.style.backgroundColor = fill;
  16089. this.frame.style.borderColor = stroke;
  16090. this.frame.style.borderWidth = strokeWidth + 'px';
  16091. this.frame.style.borderStyle = 'solid';
  16092. };
  16093. /// enumerate the available styles
  16094. Graph3d.STYLE = {
  16095. BAR: 0,
  16096. BARCOLOR: 1,
  16097. BARSIZE: 2,
  16098. DOT : 3,
  16099. DOTLINE : 4,
  16100. DOTCOLOR: 5,
  16101. DOTSIZE: 6,
  16102. GRID : 7,
  16103. LINE: 8,
  16104. SURFACE : 9
  16105. };
  16106. /**
  16107. * Retrieve the style index from given styleName
  16108. * @param {string} styleName Style name such as 'dot', 'grid', 'dot-line'
  16109. * @return {Number} styleNumber Enumeration value representing the style, or -1
  16110. * when not found
  16111. */
  16112. Graph3d.prototype._getStyleNumber = function(styleName) {
  16113. switch (styleName) {
  16114. case 'dot': return Graph3d.STYLE.DOT;
  16115. case 'dot-line': return Graph3d.STYLE.DOTLINE;
  16116. case 'dot-color': return Graph3d.STYLE.DOTCOLOR;
  16117. case 'dot-size': return Graph3d.STYLE.DOTSIZE;
  16118. case 'line': return Graph3d.STYLE.LINE;
  16119. case 'grid': return Graph3d.STYLE.GRID;
  16120. case 'surface': return Graph3d.STYLE.SURFACE;
  16121. case 'bar': return Graph3d.STYLE.BAR;
  16122. case 'bar-color': return Graph3d.STYLE.BARCOLOR;
  16123. case 'bar-size': return Graph3d.STYLE.BARSIZE;
  16124. }
  16125. return -1;
  16126. };
  16127. /**
  16128. * Determine the indexes of the data columns, based on the given style and data
  16129. * @param {DataSet} data
  16130. * @param {Number} style
  16131. */
  16132. Graph3d.prototype._determineColumnIndexes = function(data, style) {
  16133. if (this.style === Graph3d.STYLE.DOT ||
  16134. this.style === Graph3d.STYLE.DOTLINE ||
  16135. this.style === Graph3d.STYLE.LINE ||
  16136. this.style === Graph3d.STYLE.GRID ||
  16137. this.style === Graph3d.STYLE.SURFACE ||
  16138. this.style === Graph3d.STYLE.BAR) {
  16139. // 3 columns expected, and optionally a 4th with filter values
  16140. this.colX = 0;
  16141. this.colY = 1;
  16142. this.colZ = 2;
  16143. this.colValue = undefined;
  16144. if (data.getNumberOfColumns() > 3) {
  16145. this.colFilter = 3;
  16146. }
  16147. }
  16148. else if (this.style === Graph3d.STYLE.DOTCOLOR ||
  16149. this.style === Graph3d.STYLE.DOTSIZE ||
  16150. this.style === Graph3d.STYLE.BARCOLOR ||
  16151. this.style === Graph3d.STYLE.BARSIZE) {
  16152. // 4 columns expected, and optionally a 5th with filter values
  16153. this.colX = 0;
  16154. this.colY = 1;
  16155. this.colZ = 2;
  16156. this.colValue = 3;
  16157. if (data.getNumberOfColumns() > 4) {
  16158. this.colFilter = 4;
  16159. }
  16160. }
  16161. else {
  16162. throw 'Unknown style "' + this.style + '"';
  16163. }
  16164. };
  16165. Graph3d.prototype.getNumberOfRows = function(data) {
  16166. return data.length;
  16167. }
  16168. Graph3d.prototype.getNumberOfColumns = function(data) {
  16169. var counter = 0;
  16170. for (var column in data[0]) {
  16171. if (data[0].hasOwnProperty(column)) {
  16172. counter++;
  16173. }
  16174. }
  16175. return counter;
  16176. }
  16177. Graph3d.prototype.getDistinctValues = function(data, column) {
  16178. var distinctValues = [];
  16179. for (var i = 0; i < data.length; i++) {
  16180. if (distinctValues.indexOf(data[i][column]) == -1) {
  16181. distinctValues.push(data[i][column]);
  16182. }
  16183. }
  16184. return distinctValues;
  16185. }
  16186. Graph3d.prototype.getColumnRange = function(data,column) {
  16187. var minMax = {min:data[0][column],max:data[0][column]};
  16188. for (var i = 0; i < data.length; i++) {
  16189. if (minMax.min > data[i][column]) { minMax.min = data[i][column]; }
  16190. if (minMax.max < data[i][column]) { minMax.max = data[i][column]; }
  16191. }
  16192. return minMax;
  16193. };
  16194. /**
  16195. * Initialize the data from the data table. Calculate minimum and maximum values
  16196. * and column index values
  16197. * @param {Array | DataSet | DataView} rawData The data containing the items for the Graph.
  16198. * @param {Number} style Style Number
  16199. */
  16200. Graph3d.prototype._dataInitialize = function (rawData, style) {
  16201. var me = this;
  16202. // unsubscribe from the dataTable
  16203. if (this.dataSet) {
  16204. this.dataSet.off('*', this._onChange);
  16205. }
  16206. if (rawData === undefined)
  16207. return;
  16208. if (Array.isArray(rawData)) {
  16209. rawData = new DataSet(rawData);
  16210. }
  16211. var data;
  16212. if (rawData instanceof DataSet || rawData instanceof DataView) {
  16213. data = rawData.get();
  16214. }
  16215. else {
  16216. throw new Error('Array, DataSet, or DataView expected');
  16217. }
  16218. if (data.length == 0)
  16219. return;
  16220. this.dataSet = rawData;
  16221. this.dataTable = data;
  16222. // subscribe to changes in the dataset
  16223. this._onChange = function () {
  16224. me.setData(me.dataSet);
  16225. };
  16226. this.dataSet.on('*', this._onChange);
  16227. // _determineColumnIndexes
  16228. // getNumberOfRows (points)
  16229. // getNumberOfColumns (x,y,z,v,t,t1,t2...)
  16230. // getDistinctValues (unique values?)
  16231. // getColumnRange
  16232. // determine the location of x,y,z,value,filter columns
  16233. this.colX = 'x';
  16234. this.colY = 'y';
  16235. this.colZ = 'z';
  16236. this.colValue = 'style';
  16237. this.colFilter = 'filter';
  16238. // check if a filter column is provided
  16239. if (data[0].hasOwnProperty('filter')) {
  16240. if (this.dataFilter === undefined) {
  16241. this.dataFilter = new Filter(rawData, this.colFilter, this);
  16242. this.dataFilter.setOnLoadCallback(function() {me.redraw();});
  16243. }
  16244. }
  16245. var withBars = this.style == Graph3d.STYLE.BAR ||
  16246. this.style == Graph3d.STYLE.BARCOLOR ||
  16247. this.style == Graph3d.STYLE.BARSIZE;
  16248. // determine barWidth from data
  16249. if (withBars) {
  16250. if (this.defaultXBarWidth !== undefined) {
  16251. this.xBarWidth = this.defaultXBarWidth;
  16252. }
  16253. else {
  16254. var dataX = this.getDistinctValues(data,this.colX);
  16255. this.xBarWidth = (dataX[1] - dataX[0]) || 1;
  16256. }
  16257. if (this.defaultYBarWidth !== undefined) {
  16258. this.yBarWidth = this.defaultYBarWidth;
  16259. }
  16260. else {
  16261. var dataY = this.getDistinctValues(data,this.colY);
  16262. this.yBarWidth = (dataY[1] - dataY[0]) || 1;
  16263. }
  16264. }
  16265. // calculate minimums and maximums
  16266. var xRange = this.getColumnRange(data,this.colX);
  16267. if (withBars) {
  16268. xRange.min -= this.xBarWidth / 2;
  16269. xRange.max += this.xBarWidth / 2;
  16270. }
  16271. this.xMin = (this.defaultXMin !== undefined) ? this.defaultXMin : xRange.min;
  16272. this.xMax = (this.defaultXMax !== undefined) ? this.defaultXMax : xRange.max;
  16273. if (this.xMax <= this.xMin) this.xMax = this.xMin + 1;
  16274. this.xStep = (this.defaultXStep !== undefined) ? this.defaultXStep : (this.xMax-this.xMin)/5;
  16275. var yRange = this.getColumnRange(data,this.colY);
  16276. if (withBars) {
  16277. yRange.min -= this.yBarWidth / 2;
  16278. yRange.max += this.yBarWidth / 2;
  16279. }
  16280. this.yMin = (this.defaultYMin !== undefined) ? this.defaultYMin : yRange.min;
  16281. this.yMax = (this.defaultYMax !== undefined) ? this.defaultYMax : yRange.max;
  16282. if (this.yMax <= this.yMin) this.yMax = this.yMin + 1;
  16283. this.yStep = (this.defaultYStep !== undefined) ? this.defaultYStep : (this.yMax-this.yMin)/5;
  16284. var zRange = this.getColumnRange(data,this.colZ);
  16285. this.zMin = (this.defaultZMin !== undefined) ? this.defaultZMin : zRange.min;
  16286. this.zMax = (this.defaultZMax !== undefined) ? this.defaultZMax : zRange.max;
  16287. if (this.zMax <= this.zMin) this.zMax = this.zMin + 1;
  16288. this.zStep = (this.defaultZStep !== undefined) ? this.defaultZStep : (this.zMax-this.zMin)/5;
  16289. if (this.colValue !== undefined) {
  16290. var valueRange = this.getColumnRange(data,this.colValue);
  16291. this.valueMin = (this.defaultValueMin !== undefined) ? this.defaultValueMin : valueRange.min;
  16292. this.valueMax = (this.defaultValueMax !== undefined) ? this.defaultValueMax : valueRange.max;
  16293. if (this.valueMax <= this.valueMin) this.valueMax = this.valueMin + 1;
  16294. }
  16295. // set the scale dependent on the ranges.
  16296. this._setScale();
  16297. };
  16298. /**
  16299. * Filter the data based on the current filter
  16300. * @param {Array} data
  16301. * @return {Array} dataPoints Array with point objects which can be drawn on screen
  16302. */
  16303. Graph3d.prototype._getDataPoints = function (data) {
  16304. // TODO: store the created matrix dataPoints in the filters instead of reloading each time
  16305. var x, y, i, z, obj, point;
  16306. var dataPoints = [];
  16307. if (this.style === Graph3d.STYLE.GRID ||
  16308. this.style === Graph3d.STYLE.SURFACE) {
  16309. // copy all values from the google data table to a matrix
  16310. // the provided values are supposed to form a grid of (x,y) positions
  16311. // create two lists with all present x and y values
  16312. var dataX = [];
  16313. var dataY = [];
  16314. for (i = 0; i < this.getNumberOfRows(data); i++) {
  16315. x = data[i][this.colX] || 0;
  16316. y = data[i][this.colY] || 0;
  16317. if (dataX.indexOf(x) === -1) {
  16318. dataX.push(x);
  16319. }
  16320. if (dataY.indexOf(y) === -1) {
  16321. dataY.push(y);
  16322. }
  16323. }
  16324. function sortNumber(a, b) {
  16325. return a - b;
  16326. }
  16327. dataX.sort(sortNumber);
  16328. dataY.sort(sortNumber);
  16329. // create a grid, a 2d matrix, with all values.
  16330. var dataMatrix = []; // temporary data matrix
  16331. for (i = 0; i < data.length; i++) {
  16332. x = data[i][this.colX] || 0;
  16333. y = data[i][this.colY] || 0;
  16334. z = data[i][this.colZ] || 0;
  16335. var xIndex = dataX.indexOf(x); // TODO: implement Array().indexOf() for Internet Explorer
  16336. var yIndex = dataY.indexOf(y);
  16337. if (dataMatrix[xIndex] === undefined) {
  16338. dataMatrix[xIndex] = [];
  16339. }
  16340. var point3d = new Point3d();
  16341. point3d.x = x;
  16342. point3d.y = y;
  16343. point3d.z = z;
  16344. obj = {};
  16345. obj.point = point3d;
  16346. obj.trans = undefined;
  16347. obj.screen = undefined;
  16348. obj.bottom = new Point3d(x, y, this.zMin);
  16349. dataMatrix[xIndex][yIndex] = obj;
  16350. dataPoints.push(obj);
  16351. }
  16352. // fill in the pointers to the neighbors.
  16353. for (x = 0; x < dataMatrix.length; x++) {
  16354. for (y = 0; y < dataMatrix[x].length; y++) {
  16355. if (dataMatrix[x][y]) {
  16356. dataMatrix[x][y].pointRight = (x < dataMatrix.length-1) ? dataMatrix[x+1][y] : undefined;
  16357. dataMatrix[x][y].pointTop = (y < dataMatrix[x].length-1) ? dataMatrix[x][y+1] : undefined;
  16358. dataMatrix[x][y].pointCross =
  16359. (x < dataMatrix.length-1 && y < dataMatrix[x].length-1) ?
  16360. dataMatrix[x+1][y+1] :
  16361. undefined;
  16362. }
  16363. }
  16364. }
  16365. }
  16366. else { // 'dot', 'dot-line', etc.
  16367. // copy all values from the google data table to a list with Point3d objects
  16368. for (i = 0; i < data.length; i++) {
  16369. point = new Point3d();
  16370. point.x = data[i][this.colX] || 0;
  16371. point.y = data[i][this.colY] || 0;
  16372. point.z = data[i][this.colZ] || 0;
  16373. if (this.colValue !== undefined) {
  16374. point.value = data[i][this.colValue] || 0;
  16375. }
  16376. obj = {};
  16377. obj.point = point;
  16378. obj.bottom = new Point3d(point.x, point.y, this.zMin);
  16379. obj.trans = undefined;
  16380. obj.screen = undefined;
  16381. dataPoints.push(obj);
  16382. }
  16383. }
  16384. return dataPoints;
  16385. };
  16386. /**
  16387. * Append suffix 'px' to provided value x
  16388. * @param {int} x An integer value
  16389. * @return {string} the string value of x, followed by the suffix 'px'
  16390. */
  16391. Graph3d.px = function(x) {
  16392. return x + 'px';
  16393. };
  16394. /**
  16395. * Create the main frame for the Graph3d.
  16396. * This function is executed once when a Graph3d object is created. The frame
  16397. * contains a canvas, and this canvas contains all objects like the axis and
  16398. * nodes.
  16399. */
  16400. Graph3d.prototype.create = function () {
  16401. // remove all elements from the container element.
  16402. while (this.containerElement.hasChildNodes()) {
  16403. this.containerElement.removeChild(this.containerElement.firstChild);
  16404. }
  16405. this.frame = document.createElement('div');
  16406. this.frame.style.position = 'relative';
  16407. this.frame.style.overflow = 'hidden';
  16408. // create the graph canvas (HTML canvas element)
  16409. this.frame.canvas = document.createElement( 'canvas' );
  16410. this.frame.canvas.style.position = 'relative';
  16411. this.frame.appendChild(this.frame.canvas);
  16412. //if (!this.frame.canvas.getContext) {
  16413. {
  16414. var noCanvas = document.createElement( 'DIV' );
  16415. noCanvas.style.color = 'red';
  16416. noCanvas.style.fontWeight = 'bold' ;
  16417. noCanvas.style.padding = '10px';
  16418. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  16419. this.frame.canvas.appendChild(noCanvas);
  16420. }
  16421. this.frame.filter = document.createElement( 'div' );
  16422. this.frame.filter.style.position = 'absolute';
  16423. this.frame.filter.style.bottom = '0px';
  16424. this.frame.filter.style.left = '0px';
  16425. this.frame.filter.style.width = '100%';
  16426. this.frame.appendChild(this.frame.filter);
  16427. // add event listeners to handle moving and zooming the contents
  16428. var me = this;
  16429. var onmousedown = function (event) {me._onMouseDown(event);};
  16430. var ontouchstart = function (event) {me._onTouchStart(event);};
  16431. var onmousewheel = function (event) {me._onWheel(event);};
  16432. var ontooltip = function (event) {me._onTooltip(event);};
  16433. // TODO: these events are never cleaned up... can give a 'memory leakage'
  16434. G3DaddEventListener(this.frame.canvas, 'keydown', onkeydown);
  16435. G3DaddEventListener(this.frame.canvas, 'mousedown', onmousedown);
  16436. G3DaddEventListener(this.frame.canvas, 'touchstart', ontouchstart);
  16437. G3DaddEventListener(this.frame.canvas, 'mousewheel', onmousewheel);
  16438. G3DaddEventListener(this.frame.canvas, 'mousemove', ontooltip);
  16439. // add the new graph to the container element
  16440. this.containerElement.appendChild(this.frame);
  16441. };
  16442. /**
  16443. * Set a new size for the graph
  16444. * @param {string} width Width in pixels or percentage (for example '800px'
  16445. * or '50%')
  16446. * @param {string} height Height in pixels or percentage (for example '400px'
  16447. * or '30%')
  16448. */
  16449. Graph3d.prototype.setSize = function(width, height) {
  16450. this.frame.style.width = width;
  16451. this.frame.style.height = height;
  16452. this._resizeCanvas();
  16453. };
  16454. /**
  16455. * Resize the canvas to the current size of the frame
  16456. */
  16457. Graph3d.prototype._resizeCanvas = function() {
  16458. this.frame.canvas.style.width = '100%';
  16459. this.frame.canvas.style.height = '100%';
  16460. this.frame.canvas.width = this.frame.canvas.clientWidth;
  16461. this.frame.canvas.height = this.frame.canvas.clientHeight;
  16462. // adjust with for margin
  16463. this.frame.filter.style.width = (this.frame.canvas.clientWidth - 2 * 10) + 'px';
  16464. };
  16465. /**
  16466. * Start animation
  16467. */
  16468. Graph3d.prototype.animationStart = function() {
  16469. if (!this.frame.filter || !this.frame.filter.slider)
  16470. throw 'No animation available';
  16471. this.frame.filter.slider.play();
  16472. };
  16473. /**
  16474. * Stop animation
  16475. */
  16476. Graph3d.prototype.animationStop = function() {
  16477. if (!this.frame.filter || !this.frame.filter.slider) return;
  16478. this.frame.filter.slider.stop();
  16479. };
  16480. /**
  16481. * Resize the center position based on the current values in this.defaultXCenter
  16482. * and this.defaultYCenter (which are strings with a percentage or a value
  16483. * in pixels). The center positions are the variables this.xCenter
  16484. * and this.yCenter
  16485. */
  16486. Graph3d.prototype._resizeCenter = function() {
  16487. // calculate the horizontal center position
  16488. if (this.defaultXCenter.charAt(this.defaultXCenter.length-1) === '%') {
  16489. this.xcenter =
  16490. parseFloat(this.defaultXCenter) / 100 *
  16491. this.frame.canvas.clientWidth;
  16492. }
  16493. else {
  16494. this.xcenter = parseFloat(this.defaultXCenter); // supposed to be in px
  16495. }
  16496. // calculate the vertical center position
  16497. if (this.defaultYCenter.charAt(this.defaultYCenter.length-1) === '%') {
  16498. this.ycenter =
  16499. parseFloat(this.defaultYCenter) / 100 *
  16500. (this.frame.canvas.clientHeight - this.frame.filter.clientHeight);
  16501. }
  16502. else {
  16503. this.ycenter = parseFloat(this.defaultYCenter); // supposed to be in px
  16504. }
  16505. };
  16506. /**
  16507. * Set the rotation and distance of the camera
  16508. * @param {Object} pos An object with the camera position. The object
  16509. * contains three parameters:
  16510. * - horizontal {Number}
  16511. * The horizontal rotation, between 0 and 2*PI.
  16512. * Optional, can be left undefined.
  16513. * - vertical {Number}
  16514. * The vertical rotation, between 0 and 0.5*PI
  16515. * if vertical=0.5*PI, the graph is shown from the
  16516. * top. Optional, can be left undefined.
  16517. * - distance {Number}
  16518. * The (normalized) distance of the camera to the
  16519. * center of the graph, a value between 0.71 and 5.0.
  16520. * Optional, can be left undefined.
  16521. */
  16522. Graph3d.prototype.setCameraPosition = function(pos) {
  16523. if (pos === undefined) {
  16524. return;
  16525. }
  16526. if (pos.horizontal !== undefined && pos.vertical !== undefined) {
  16527. this.camera.setArmRotation(pos.horizontal, pos.vertical);
  16528. }
  16529. if (pos.distance !== undefined) {
  16530. this.camera.setArmLength(pos.distance);
  16531. }
  16532. this.redraw();
  16533. };
  16534. /**
  16535. * Retrieve the current camera rotation
  16536. * @return {object} An object with parameters horizontal, vertical, and
  16537. * distance
  16538. */
  16539. Graph3d.prototype.getCameraPosition = function() {
  16540. var pos = this.camera.getArmRotation();
  16541. pos.distance = this.camera.getArmLength();
  16542. return pos;
  16543. };
  16544. /**
  16545. * Load data into the 3D Graph
  16546. */
  16547. Graph3d.prototype._readData = function(data) {
  16548. // read the data
  16549. this._dataInitialize(data, this.style);
  16550. if (this.dataFilter) {
  16551. // apply filtering
  16552. this.dataPoints = this.dataFilter._getDataPoints();
  16553. }
  16554. else {
  16555. // no filtering. load all data
  16556. this.dataPoints = this._getDataPoints(this.dataTable);
  16557. }
  16558. // draw the filter
  16559. this._redrawFilter();
  16560. };
  16561. /**
  16562. * Replace the dataset of the Graph3d
  16563. * @param {Array | DataSet | DataView} data
  16564. */
  16565. Graph3d.prototype.setData = function (data) {
  16566. this._readData(data);
  16567. this.redraw();
  16568. // start animation when option is true
  16569. if (this.animationAutoStart && this.dataFilter) {
  16570. this.animationStart();
  16571. }
  16572. };
  16573. /**
  16574. * Update the options. Options will be merged with current options
  16575. * @param {Object} options
  16576. */
  16577. Graph3d.prototype.setOptions = function (options) {
  16578. var cameraPosition = undefined;
  16579. this.animationStop();
  16580. if (options !== undefined) {
  16581. // retrieve parameter values
  16582. if (options.width !== undefined) this.width = options.width;
  16583. if (options.height !== undefined) this.height = options.height;
  16584. if (options.xCenter !== undefined) this.defaultXCenter = options.xCenter;
  16585. if (options.yCenter !== undefined) this.defaultYCenter = options.yCenter;
  16586. if (options.filterLabel !== undefined) this.filterLabel = options.filterLabel;
  16587. if (options.legendLabel !== undefined) this.legendLabel = options.legendLabel;
  16588. if (options.xLabel !== undefined) this.xLabel = options.xLabel;
  16589. if (options.yLabel !== undefined) this.yLabel = options.yLabel;
  16590. if (options.zLabel !== undefined) this.zLabel = options.zLabel;
  16591. if (options.style !== undefined) {
  16592. var styleNumber = this._getStyleNumber(options.style);
  16593. if (styleNumber !== -1) {
  16594. this.style = styleNumber;
  16595. }
  16596. }
  16597. if (options.showGrid !== undefined) this.showGrid = options.showGrid;
  16598. if (options.showPerspective !== undefined) this.showPerspective = options.showPerspective;
  16599. if (options.showShadow !== undefined) this.showShadow = options.showShadow;
  16600. if (options.tooltip !== undefined) this.showTooltip = options.tooltip;
  16601. if (options.showAnimationControls !== undefined) this.showAnimationControls = options.showAnimationControls;
  16602. if (options.keepAspectRatio !== undefined) this.keepAspectRatio = options.keepAspectRatio;
  16603. if (options.verticalRatio !== undefined) this.verticalRatio = options.verticalRatio;
  16604. if (options.animationInterval !== undefined) this.animationInterval = options.animationInterval;
  16605. if (options.animationPreload !== undefined) this.animationPreload = options.animationPreload;
  16606. if (options.animationAutoStart !== undefined)this.animationAutoStart = options.animationAutoStart;
  16607. if (options.xBarWidth !== undefined) this.defaultXBarWidth = options.xBarWidth;
  16608. if (options.yBarWidth !== undefined) this.defaultYBarWidth = options.yBarWidth;
  16609. if (options.xMin !== undefined) this.defaultXMin = options.xMin;
  16610. if (options.xStep !== undefined) this.defaultXStep = options.xStep;
  16611. if (options.xMax !== undefined) this.defaultXMax = options.xMax;
  16612. if (options.yMin !== undefined) this.defaultYMin = options.yMin;
  16613. if (options.yStep !== undefined) this.defaultYStep = options.yStep;
  16614. if (options.yMax !== undefined) this.defaultYMax = options.yMax;
  16615. if (options.zMin !== undefined) this.defaultZMin = options.zMin;
  16616. if (options.zStep !== undefined) this.defaultZStep = options.zStep;
  16617. if (options.zMax !== undefined) this.defaultZMax = options.zMax;
  16618. if (options.valueMin !== undefined) this.defaultValueMin = options.valueMin;
  16619. if (options.valueMax !== undefined) this.defaultValueMax = options.valueMax;
  16620. if (options.cameraPosition !== undefined) cameraPosition = options.cameraPosition;
  16621. if (cameraPosition !== undefined) {
  16622. this.camera.setArmRotation(cameraPosition.horizontal, cameraPosition.vertical);
  16623. this.camera.setArmLength(cameraPosition.distance);
  16624. }
  16625. else {
  16626. this.camera.setArmRotation(1.0, 0.5);
  16627. this.camera.setArmLength(1.7);
  16628. }
  16629. }
  16630. this._setBackgroundColor(options && options.backgroundColor);
  16631. this.setSize(this.width, this.height);
  16632. // re-load the data
  16633. if (this.dataTable) {
  16634. this.setData(this.dataTable);
  16635. }
  16636. // start animation when option is true
  16637. if (this.animationAutoStart && this.dataFilter) {
  16638. this.animationStart();
  16639. }
  16640. };
  16641. /**
  16642. * Redraw the Graph.
  16643. */
  16644. Graph3d.prototype.redraw = function() {
  16645. if (this.dataPoints === undefined) {
  16646. throw 'Error: graph data not initialized';
  16647. }
  16648. this._resizeCanvas();
  16649. this._resizeCenter();
  16650. this._redrawSlider();
  16651. this._redrawClear();
  16652. this._redrawAxis();
  16653. if (this.style === Graph3d.STYLE.GRID ||
  16654. this.style === Graph3d.STYLE.SURFACE) {
  16655. this._redrawDataGrid();
  16656. }
  16657. else if (this.style === Graph3d.STYLE.LINE) {
  16658. this._redrawDataLine();
  16659. }
  16660. else if (this.style === Graph3d.STYLE.BAR ||
  16661. this.style === Graph3d.STYLE.BARCOLOR ||
  16662. this.style === Graph3d.STYLE.BARSIZE) {
  16663. this._redrawDataBar();
  16664. }
  16665. else {
  16666. // style is DOT, DOTLINE, DOTCOLOR, DOTSIZE
  16667. this._redrawDataDot();
  16668. }
  16669. this._redrawInfo();
  16670. this._redrawLegend();
  16671. };
  16672. /**
  16673. * Clear the canvas before redrawing
  16674. */
  16675. Graph3d.prototype._redrawClear = function() {
  16676. var canvas = this.frame.canvas;
  16677. var ctx = canvas.getContext('2d');
  16678. ctx.clearRect(0, 0, canvas.width, canvas.height);
  16679. };
  16680. /**
  16681. * Redraw the legend showing the colors
  16682. */
  16683. Graph3d.prototype._redrawLegend = function() {
  16684. var y;
  16685. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  16686. this.style === Graph3d.STYLE.DOTSIZE) {
  16687. var dotSize = this.frame.clientWidth * 0.02;
  16688. var widthMin, widthMax;
  16689. if (this.style === Graph3d.STYLE.DOTSIZE) {
  16690. widthMin = dotSize / 2; // px
  16691. widthMax = dotSize / 2 + dotSize * 2; // Todo: put this in one function
  16692. }
  16693. else {
  16694. widthMin = 20; // px
  16695. widthMax = 20; // px
  16696. }
  16697. var height = Math.max(this.frame.clientHeight * 0.25, 100);
  16698. var top = this.margin;
  16699. var right = this.frame.clientWidth - this.margin;
  16700. var left = right - widthMax;
  16701. var bottom = top + height;
  16702. }
  16703. var canvas = this.frame.canvas;
  16704. var ctx = canvas.getContext('2d');
  16705. ctx.lineWidth = 1;
  16706. ctx.font = '14px arial'; // TODO: put in options
  16707. if (this.style === Graph3d.STYLE.DOTCOLOR) {
  16708. // draw the color bar
  16709. var ymin = 0;
  16710. var ymax = height; // Todo: make height customizable
  16711. for (y = ymin; y < ymax; y++) {
  16712. var f = (y - ymin) / (ymax - ymin);
  16713. //var width = (dotSize / 2 + (1-f) * dotSize * 2); // Todo: put this in one function
  16714. var hue = f * 240;
  16715. var color = this._hsv2rgb(hue, 1, 1);
  16716. ctx.strokeStyle = color;
  16717. ctx.beginPath();
  16718. ctx.moveTo(left, top + y);
  16719. ctx.lineTo(right, top + y);
  16720. ctx.stroke();
  16721. }
  16722. ctx.strokeStyle = this.colorAxis;
  16723. ctx.strokeRect(left, top, widthMax, height);
  16724. }
  16725. if (this.style === Graph3d.STYLE.DOTSIZE) {
  16726. // draw border around color bar
  16727. ctx.strokeStyle = this.colorAxis;
  16728. ctx.fillStyle = this.colorDot;
  16729. ctx.beginPath();
  16730. ctx.moveTo(left, top);
  16731. ctx.lineTo(right, top);
  16732. ctx.lineTo(right - widthMax + widthMin, bottom);
  16733. ctx.lineTo(left, bottom);
  16734. ctx.closePath();
  16735. ctx.fill();
  16736. ctx.stroke();
  16737. }
  16738. if (this.style === Graph3d.STYLE.DOTCOLOR ||
  16739. this.style === Graph3d.STYLE.DOTSIZE) {
  16740. // print values along the color bar
  16741. var gridLineLen = 5; // px
  16742. var step = new StepNumber(this.valueMin, this.valueMax, (this.valueMax-this.valueMin)/5, true);
  16743. step.start();
  16744. if (step.getCurrent() < this.valueMin) {
  16745. step.next();
  16746. }
  16747. while (!step.end()) {
  16748. y = bottom - (step.getCurrent() - this.valueMin) / (this.valueMax - this.valueMin) * height;
  16749. ctx.beginPath();
  16750. ctx.moveTo(left - gridLineLen, y);
  16751. ctx.lineTo(left, y);
  16752. ctx.stroke();
  16753. ctx.textAlign = 'right';
  16754. ctx.textBaseline = 'middle';
  16755. ctx.fillStyle = this.colorAxis;
  16756. ctx.fillText(step.getCurrent(), left - 2 * gridLineLen, y);
  16757. step.next();
  16758. }
  16759. ctx.textAlign = 'right';
  16760. ctx.textBaseline = 'top';
  16761. var label = this.legendLabel;
  16762. ctx.fillText(label, right, bottom + this.margin);
  16763. }
  16764. };
  16765. /**
  16766. * Redraw the filter
  16767. */
  16768. Graph3d.prototype._redrawFilter = function() {
  16769. this.frame.filter.innerHTML = '';
  16770. if (this.dataFilter) {
  16771. var options = {
  16772. 'visible': this.showAnimationControls
  16773. };
  16774. var slider = new Slider(this.frame.filter, options);
  16775. this.frame.filter.slider = slider;
  16776. // TODO: css here is not nice here...
  16777. this.frame.filter.style.padding = '10px';
  16778. //this.frame.filter.style.backgroundColor = '#EFEFEF';
  16779. slider.setValues(this.dataFilter.values);
  16780. slider.setPlayInterval(this.animationInterval);
  16781. // create an event handler
  16782. var me = this;
  16783. var onchange = function () {
  16784. var index = slider.getIndex();
  16785. me.dataFilter.selectValue(index);
  16786. me.dataPoints = me.dataFilter._getDataPoints();
  16787. me.redraw();
  16788. };
  16789. slider.setOnChangeCallback(onchange);
  16790. }
  16791. else {
  16792. this.frame.filter.slider = undefined;
  16793. }
  16794. };
  16795. /**
  16796. * Redraw the slider
  16797. */
  16798. Graph3d.prototype._redrawSlider = function() {
  16799. if ( this.frame.filter.slider !== undefined) {
  16800. this.frame.filter.slider.redraw();
  16801. }
  16802. };
  16803. /**
  16804. * Redraw common information
  16805. */
  16806. Graph3d.prototype._redrawInfo = function() {
  16807. if (this.dataFilter) {
  16808. var canvas = this.frame.canvas;
  16809. var ctx = canvas.getContext('2d');
  16810. ctx.font = '14px arial'; // TODO: put in options
  16811. ctx.lineStyle = 'gray';
  16812. ctx.fillStyle = 'gray';
  16813. ctx.textAlign = 'left';
  16814. ctx.textBaseline = 'top';
  16815. var x = this.margin;
  16816. var y = this.margin;
  16817. ctx.fillText(this.dataFilter.getLabel() + ': ' + this.dataFilter.getSelectedValue(), x, y);
  16818. }
  16819. };
  16820. /**
  16821. * Redraw the axis
  16822. */
  16823. Graph3d.prototype._redrawAxis = function() {
  16824. var canvas = this.frame.canvas,
  16825. ctx = canvas.getContext('2d'),
  16826. from, to, step, prettyStep,
  16827. text, xText, yText, zText,
  16828. offset, xOffset, yOffset,
  16829. xMin2d, xMax2d;
  16830. // TODO: get the actual rendered style of the containerElement
  16831. //ctx.font = this.containerElement.style.font;
  16832. ctx.font = 24 / this.camera.getArmLength() + 'px arial';
  16833. // calculate the length for the short grid lines
  16834. var gridLenX = 0.025 / this.scale.x;
  16835. var gridLenY = 0.025 / this.scale.y;
  16836. var textMargin = 5 / this.camera.getArmLength(); // px
  16837. var armAngle = this.camera.getArmRotation().horizontal;
  16838. // draw x-grid lines
  16839. ctx.lineWidth = 1;
  16840. prettyStep = (this.defaultXStep === undefined);
  16841. step = new StepNumber(this.xMin, this.xMax, this.xStep, prettyStep);
  16842. step.start();
  16843. if (step.getCurrent() < this.xMin) {
  16844. step.next();
  16845. }
  16846. while (!step.end()) {
  16847. var x = step.getCurrent();
  16848. if (this.showGrid) {
  16849. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  16850. to = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  16851. ctx.strokeStyle = this.colorGrid;
  16852. ctx.beginPath();
  16853. ctx.moveTo(from.x, from.y);
  16854. ctx.lineTo(to.x, to.y);
  16855. ctx.stroke();
  16856. }
  16857. else {
  16858. from = this._convert3Dto2D(new Point3d(x, this.yMin, this.zMin));
  16859. to = this._convert3Dto2D(new Point3d(x, this.yMin+gridLenX, this.zMin));
  16860. ctx.strokeStyle = this.colorAxis;
  16861. ctx.beginPath();
  16862. ctx.moveTo(from.x, from.y);
  16863. ctx.lineTo(to.x, to.y);
  16864. ctx.stroke();
  16865. from = this._convert3Dto2D(new Point3d(x, this.yMax, this.zMin));
  16866. to = this._convert3Dto2D(new Point3d(x, this.yMax-gridLenX, this.zMin));
  16867. ctx.strokeStyle = this.colorAxis;
  16868. ctx.beginPath();
  16869. ctx.moveTo(from.x, from.y);
  16870. ctx.lineTo(to.x, to.y);
  16871. ctx.stroke();
  16872. }
  16873. yText = (Math.cos(armAngle) > 0) ? this.yMin : this.yMax;
  16874. text = this._convert3Dto2D(new Point3d(x, yText, this.zMin));
  16875. if (Math.cos(armAngle * 2) > 0) {
  16876. ctx.textAlign = 'center';
  16877. ctx.textBaseline = 'top';
  16878. text.y += textMargin;
  16879. }
  16880. else if (Math.sin(armAngle * 2) < 0){
  16881. ctx.textAlign = 'right';
  16882. ctx.textBaseline = 'middle';
  16883. }
  16884. else {
  16885. ctx.textAlign = 'left';
  16886. ctx.textBaseline = 'middle';
  16887. }
  16888. ctx.fillStyle = this.colorAxis;
  16889. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  16890. step.next();
  16891. }
  16892. // draw y-grid lines
  16893. ctx.lineWidth = 1;
  16894. prettyStep = (this.defaultYStep === undefined);
  16895. step = new StepNumber(this.yMin, this.yMax, this.yStep, prettyStep);
  16896. step.start();
  16897. if (step.getCurrent() < this.yMin) {
  16898. step.next();
  16899. }
  16900. while (!step.end()) {
  16901. if (this.showGrid) {
  16902. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  16903. to = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  16904. ctx.strokeStyle = this.colorGrid;
  16905. ctx.beginPath();
  16906. ctx.moveTo(from.x, from.y);
  16907. ctx.lineTo(to.x, to.y);
  16908. ctx.stroke();
  16909. }
  16910. else {
  16911. from = this._convert3Dto2D(new Point3d(this.xMin, step.getCurrent(), this.zMin));
  16912. to = this._convert3Dto2D(new Point3d(this.xMin+gridLenY, step.getCurrent(), this.zMin));
  16913. ctx.strokeStyle = this.colorAxis;
  16914. ctx.beginPath();
  16915. ctx.moveTo(from.x, from.y);
  16916. ctx.lineTo(to.x, to.y);
  16917. ctx.stroke();
  16918. from = this._convert3Dto2D(new Point3d(this.xMax, step.getCurrent(), this.zMin));
  16919. to = this._convert3Dto2D(new Point3d(this.xMax-gridLenY, step.getCurrent(), this.zMin));
  16920. ctx.strokeStyle = this.colorAxis;
  16921. ctx.beginPath();
  16922. ctx.moveTo(from.x, from.y);
  16923. ctx.lineTo(to.x, to.y);
  16924. ctx.stroke();
  16925. }
  16926. xText = (Math.sin(armAngle ) > 0) ? this.xMin : this.xMax;
  16927. text = this._convert3Dto2D(new Point3d(xText, step.getCurrent(), this.zMin));
  16928. if (Math.cos(armAngle * 2) < 0) {
  16929. ctx.textAlign = 'center';
  16930. ctx.textBaseline = 'top';
  16931. text.y += textMargin;
  16932. }
  16933. else if (Math.sin(armAngle * 2) > 0){
  16934. ctx.textAlign = 'right';
  16935. ctx.textBaseline = 'middle';
  16936. }
  16937. else {
  16938. ctx.textAlign = 'left';
  16939. ctx.textBaseline = 'middle';
  16940. }
  16941. ctx.fillStyle = this.colorAxis;
  16942. ctx.fillText(' ' + step.getCurrent() + ' ', text.x, text.y);
  16943. step.next();
  16944. }
  16945. // draw z-grid lines and axis
  16946. ctx.lineWidth = 1;
  16947. prettyStep = (this.defaultZStep === undefined);
  16948. step = new StepNumber(this.zMin, this.zMax, this.zStep, prettyStep);
  16949. step.start();
  16950. if (step.getCurrent() < this.zMin) {
  16951. step.next();
  16952. }
  16953. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  16954. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  16955. while (!step.end()) {
  16956. // TODO: make z-grid lines really 3d?
  16957. from = this._convert3Dto2D(new Point3d(xText, yText, step.getCurrent()));
  16958. ctx.strokeStyle = this.colorAxis;
  16959. ctx.beginPath();
  16960. ctx.moveTo(from.x, from.y);
  16961. ctx.lineTo(from.x - textMargin, from.y);
  16962. ctx.stroke();
  16963. ctx.textAlign = 'right';
  16964. ctx.textBaseline = 'middle';
  16965. ctx.fillStyle = this.colorAxis;
  16966. ctx.fillText(step.getCurrent() + ' ', from.x - 5, from.y);
  16967. step.next();
  16968. }
  16969. ctx.lineWidth = 1;
  16970. from = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  16971. to = this._convert3Dto2D(new Point3d(xText, yText, this.zMax));
  16972. ctx.strokeStyle = this.colorAxis;
  16973. ctx.beginPath();
  16974. ctx.moveTo(from.x, from.y);
  16975. ctx.lineTo(to.x, to.y);
  16976. ctx.stroke();
  16977. // draw x-axis
  16978. ctx.lineWidth = 1;
  16979. // line at yMin
  16980. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  16981. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  16982. ctx.strokeStyle = this.colorAxis;
  16983. ctx.beginPath();
  16984. ctx.moveTo(xMin2d.x, xMin2d.y);
  16985. ctx.lineTo(xMax2d.x, xMax2d.y);
  16986. ctx.stroke();
  16987. // line at ymax
  16988. xMin2d = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  16989. xMax2d = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  16990. ctx.strokeStyle = this.colorAxis;
  16991. ctx.beginPath();
  16992. ctx.moveTo(xMin2d.x, xMin2d.y);
  16993. ctx.lineTo(xMax2d.x, xMax2d.y);
  16994. ctx.stroke();
  16995. // draw y-axis
  16996. ctx.lineWidth = 1;
  16997. // line at xMin
  16998. from = this._convert3Dto2D(new Point3d(this.xMin, this.yMin, this.zMin));
  16999. to = this._convert3Dto2D(new Point3d(this.xMin, this.yMax, this.zMin));
  17000. ctx.strokeStyle = this.colorAxis;
  17001. ctx.beginPath();
  17002. ctx.moveTo(from.x, from.y);
  17003. ctx.lineTo(to.x, to.y);
  17004. ctx.stroke();
  17005. // line at xMax
  17006. from = this._convert3Dto2D(new Point3d(this.xMax, this.yMin, this.zMin));
  17007. to = this._convert3Dto2D(new Point3d(this.xMax, this.yMax, this.zMin));
  17008. ctx.strokeStyle = this.colorAxis;
  17009. ctx.beginPath();
  17010. ctx.moveTo(from.x, from.y);
  17011. ctx.lineTo(to.x, to.y);
  17012. ctx.stroke();
  17013. // draw x-label
  17014. var xLabel = this.xLabel;
  17015. if (xLabel.length > 0) {
  17016. yOffset = 0.1 / this.scale.y;
  17017. xText = (this.xMin + this.xMax) / 2;
  17018. yText = (Math.cos(armAngle) > 0) ? this.yMin - yOffset: this.yMax + yOffset;
  17019. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  17020. if (Math.cos(armAngle * 2) > 0) {
  17021. ctx.textAlign = 'center';
  17022. ctx.textBaseline = 'top';
  17023. }
  17024. else if (Math.sin(armAngle * 2) < 0){
  17025. ctx.textAlign = 'right';
  17026. ctx.textBaseline = 'middle';
  17027. }
  17028. else {
  17029. ctx.textAlign = 'left';
  17030. ctx.textBaseline = 'middle';
  17031. }
  17032. ctx.fillStyle = this.colorAxis;
  17033. ctx.fillText(xLabel, text.x, text.y);
  17034. }
  17035. // draw y-label
  17036. var yLabel = this.yLabel;
  17037. if (yLabel.length > 0) {
  17038. xOffset = 0.1 / this.scale.x;
  17039. xText = (Math.sin(armAngle ) > 0) ? this.xMin - xOffset : this.xMax + xOffset;
  17040. yText = (this.yMin + this.yMax) / 2;
  17041. text = this._convert3Dto2D(new Point3d(xText, yText, this.zMin));
  17042. if (Math.cos(armAngle * 2) < 0) {
  17043. ctx.textAlign = 'center';
  17044. ctx.textBaseline = 'top';
  17045. }
  17046. else if (Math.sin(armAngle * 2) > 0){
  17047. ctx.textAlign = 'right';
  17048. ctx.textBaseline = 'middle';
  17049. }
  17050. else {
  17051. ctx.textAlign = 'left';
  17052. ctx.textBaseline = 'middle';
  17053. }
  17054. ctx.fillStyle = this.colorAxis;
  17055. ctx.fillText(yLabel, text.x, text.y);
  17056. }
  17057. // draw z-label
  17058. var zLabel = this.zLabel;
  17059. if (zLabel.length > 0) {
  17060. offset = 30; // pixels. // TODO: relate to the max width of the values on the z axis?
  17061. xText = (Math.cos(armAngle ) > 0) ? this.xMin : this.xMax;
  17062. yText = (Math.sin(armAngle ) < 0) ? this.yMin : this.yMax;
  17063. zText = (this.zMin + this.zMax) / 2;
  17064. text = this._convert3Dto2D(new Point3d(xText, yText, zText));
  17065. ctx.textAlign = 'right';
  17066. ctx.textBaseline = 'middle';
  17067. ctx.fillStyle = this.colorAxis;
  17068. ctx.fillText(zLabel, text.x - offset, text.y);
  17069. }
  17070. };
  17071. /**
  17072. * Calculate the color based on the given value.
  17073. * @param {Number} H Hue, a value be between 0 and 360
  17074. * @param {Number} S Saturation, a value between 0 and 1
  17075. * @param {Number} V Value, a value between 0 and 1
  17076. */
  17077. Graph3d.prototype._hsv2rgb = function(H, S, V) {
  17078. var R, G, B, C, Hi, X;
  17079. C = V * S;
  17080. Hi = Math.floor(H/60); // hi = 0,1,2,3,4,5
  17081. X = C * (1 - Math.abs(((H/60) % 2) - 1));
  17082. switch (Hi) {
  17083. case 0: R = C; G = X; B = 0; break;
  17084. case 1: R = X; G = C; B = 0; break;
  17085. case 2: R = 0; G = C; B = X; break;
  17086. case 3: R = 0; G = X; B = C; break;
  17087. case 4: R = X; G = 0; B = C; break;
  17088. case 5: R = C; G = 0; B = X; break;
  17089. default: R = 0; G = 0; B = 0; break;
  17090. }
  17091. return 'RGB(' + parseInt(R*255) + ',' + parseInt(G*255) + ',' + parseInt(B*255) + ')';
  17092. };
  17093. /**
  17094. * Draw all datapoints as a grid
  17095. * This function can be used when the style is 'grid'
  17096. */
  17097. Graph3d.prototype._redrawDataGrid = function() {
  17098. var canvas = this.frame.canvas,
  17099. ctx = canvas.getContext('2d'),
  17100. point, right, top, cross,
  17101. i,
  17102. topSideVisible, fillStyle, strokeStyle, lineWidth,
  17103. h, s, v, zAvg;
  17104. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17105. return; // TODO: throw exception?
  17106. // calculate the translations and screen position of all points
  17107. for (i = 0; i < this.dataPoints.length; i++) {
  17108. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17109. var screen = this._convertTranslationToScreen(trans);
  17110. this.dataPoints[i].trans = trans;
  17111. this.dataPoints[i].screen = screen;
  17112. // calculate the translation of the point at the bottom (needed for sorting)
  17113. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  17114. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  17115. }
  17116. // sort the points on depth of their (x,y) position (not on z)
  17117. var sortDepth = function (a, b) {
  17118. return b.dist - a.dist;
  17119. };
  17120. this.dataPoints.sort(sortDepth);
  17121. if (this.style === Graph3d.STYLE.SURFACE) {
  17122. for (i = 0; i < this.dataPoints.length; i++) {
  17123. point = this.dataPoints[i];
  17124. right = this.dataPoints[i].pointRight;
  17125. top = this.dataPoints[i].pointTop;
  17126. cross = this.dataPoints[i].pointCross;
  17127. if (point !== undefined && right !== undefined && top !== undefined && cross !== undefined) {
  17128. if (this.showGrayBottom || this.showShadow) {
  17129. // calculate the cross product of the two vectors from center
  17130. // to left and right, in order to know whether we are looking at the
  17131. // bottom or at the top side. We can also use the cross product
  17132. // for calculating light intensity
  17133. var aDiff = Point3d.subtract(cross.trans, point.trans);
  17134. var bDiff = Point3d.subtract(top.trans, right.trans);
  17135. var crossproduct = Point3d.crossProduct(aDiff, bDiff);
  17136. var len = crossproduct.length();
  17137. // FIXME: there is a bug with determining the surface side (shadow or colored)
  17138. topSideVisible = (crossproduct.z > 0);
  17139. }
  17140. else {
  17141. topSideVisible = true;
  17142. }
  17143. if (topSideVisible) {
  17144. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17145. zAvg = (point.point.z + right.point.z + top.point.z + cross.point.z) / 4;
  17146. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17147. s = 1; // saturation
  17148. if (this.showShadow) {
  17149. v = Math.min(1 + (crossproduct.x / len) / 2, 1); // value. TODO: scale
  17150. fillStyle = this._hsv2rgb(h, s, v);
  17151. strokeStyle = fillStyle;
  17152. }
  17153. else {
  17154. v = 1;
  17155. fillStyle = this._hsv2rgb(h, s, v);
  17156. strokeStyle = this.colorAxis;
  17157. }
  17158. }
  17159. else {
  17160. fillStyle = 'gray';
  17161. strokeStyle = this.colorAxis;
  17162. }
  17163. lineWidth = 0.5;
  17164. ctx.lineWidth = lineWidth;
  17165. ctx.fillStyle = fillStyle;
  17166. ctx.strokeStyle = strokeStyle;
  17167. ctx.beginPath();
  17168. ctx.moveTo(point.screen.x, point.screen.y);
  17169. ctx.lineTo(right.screen.x, right.screen.y);
  17170. ctx.lineTo(cross.screen.x, cross.screen.y);
  17171. ctx.lineTo(top.screen.x, top.screen.y);
  17172. ctx.closePath();
  17173. ctx.fill();
  17174. ctx.stroke();
  17175. }
  17176. }
  17177. }
  17178. else { // grid style
  17179. for (i = 0; i < this.dataPoints.length; i++) {
  17180. point = this.dataPoints[i];
  17181. right = this.dataPoints[i].pointRight;
  17182. top = this.dataPoints[i].pointTop;
  17183. if (point !== undefined) {
  17184. if (this.showPerspective) {
  17185. lineWidth = 2 / -point.trans.z;
  17186. }
  17187. else {
  17188. lineWidth = 2 * -(this.eye.z / this.camera.getArmLength());
  17189. }
  17190. }
  17191. if (point !== undefined && right !== undefined) {
  17192. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17193. zAvg = (point.point.z + right.point.z) / 2;
  17194. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17195. ctx.lineWidth = lineWidth;
  17196. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  17197. ctx.beginPath();
  17198. ctx.moveTo(point.screen.x, point.screen.y);
  17199. ctx.lineTo(right.screen.x, right.screen.y);
  17200. ctx.stroke();
  17201. }
  17202. if (point !== undefined && top !== undefined) {
  17203. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17204. zAvg = (point.point.z + top.point.z) / 2;
  17205. h = (1 - (zAvg - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17206. ctx.lineWidth = lineWidth;
  17207. ctx.strokeStyle = this._hsv2rgb(h, 1, 1);
  17208. ctx.beginPath();
  17209. ctx.moveTo(point.screen.x, point.screen.y);
  17210. ctx.lineTo(top.screen.x, top.screen.y);
  17211. ctx.stroke();
  17212. }
  17213. }
  17214. }
  17215. };
  17216. /**
  17217. * Draw all datapoints as dots.
  17218. * This function can be used when the style is 'dot' or 'dot-line'
  17219. */
  17220. Graph3d.prototype._redrawDataDot = function() {
  17221. var canvas = this.frame.canvas;
  17222. var ctx = canvas.getContext('2d');
  17223. var i;
  17224. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17225. return; // TODO: throw exception?
  17226. // calculate the translations of all points
  17227. for (i = 0; i < this.dataPoints.length; i++) {
  17228. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17229. var screen = this._convertTranslationToScreen(trans);
  17230. this.dataPoints[i].trans = trans;
  17231. this.dataPoints[i].screen = screen;
  17232. // calculate the distance from the point at the bottom to the camera
  17233. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  17234. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  17235. }
  17236. // order the translated points by depth
  17237. var sortDepth = function (a, b) {
  17238. return b.dist - a.dist;
  17239. };
  17240. this.dataPoints.sort(sortDepth);
  17241. // draw the datapoints as colored circles
  17242. var dotSize = this.frame.clientWidth * 0.02; // px
  17243. for (i = 0; i < this.dataPoints.length; i++) {
  17244. var point = this.dataPoints[i];
  17245. if (this.style === Graph3d.STYLE.DOTLINE) {
  17246. // draw a vertical line from the bottom to the graph value
  17247. //var from = this._convert3Dto2D(new Point3d(point.point.x, point.point.y, this.zMin));
  17248. var from = this._convert3Dto2D(point.bottom);
  17249. ctx.lineWidth = 1;
  17250. ctx.strokeStyle = this.colorGrid;
  17251. ctx.beginPath();
  17252. ctx.moveTo(from.x, from.y);
  17253. ctx.lineTo(point.screen.x, point.screen.y);
  17254. ctx.stroke();
  17255. }
  17256. // calculate radius for the circle
  17257. var size;
  17258. if (this.style === Graph3d.STYLE.DOTSIZE) {
  17259. size = dotSize/2 + 2*dotSize * (point.point.value - this.valueMin) / (this.valueMax - this.valueMin);
  17260. }
  17261. else {
  17262. size = dotSize;
  17263. }
  17264. var radius;
  17265. if (this.showPerspective) {
  17266. radius = size / -point.trans.z;
  17267. }
  17268. else {
  17269. radius = size * -(this.eye.z / this.camera.getArmLength());
  17270. }
  17271. if (radius < 0) {
  17272. radius = 0;
  17273. }
  17274. var hue, color, borderColor;
  17275. if (this.style === Graph3d.STYLE.DOTCOLOR ) {
  17276. // calculate the color based on the value
  17277. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  17278. color = this._hsv2rgb(hue, 1, 1);
  17279. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17280. }
  17281. else if (this.style === Graph3d.STYLE.DOTSIZE) {
  17282. color = this.colorDot;
  17283. borderColor = this.colorDotBorder;
  17284. }
  17285. else {
  17286. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17287. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17288. color = this._hsv2rgb(hue, 1, 1);
  17289. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17290. }
  17291. // draw the circle
  17292. ctx.lineWidth = 1.0;
  17293. ctx.strokeStyle = borderColor;
  17294. ctx.fillStyle = color;
  17295. ctx.beginPath();
  17296. ctx.arc(point.screen.x, point.screen.y, radius, 0, Math.PI*2, true);
  17297. ctx.fill();
  17298. ctx.stroke();
  17299. }
  17300. };
  17301. /**
  17302. * Draw all datapoints as bars.
  17303. * This function can be used when the style is 'bar', 'bar-color', or 'bar-size'
  17304. */
  17305. Graph3d.prototype._redrawDataBar = function() {
  17306. var canvas = this.frame.canvas;
  17307. var ctx = canvas.getContext('2d');
  17308. var i, j, surface, corners;
  17309. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17310. return; // TODO: throw exception?
  17311. // calculate the translations of all points
  17312. for (i = 0; i < this.dataPoints.length; i++) {
  17313. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17314. var screen = this._convertTranslationToScreen(trans);
  17315. this.dataPoints[i].trans = trans;
  17316. this.dataPoints[i].screen = screen;
  17317. // calculate the distance from the point at the bottom to the camera
  17318. var transBottom = this._convertPointToTranslation(this.dataPoints[i].bottom);
  17319. this.dataPoints[i].dist = this.showPerspective ? transBottom.length() : -transBottom.z;
  17320. }
  17321. // order the translated points by depth
  17322. var sortDepth = function (a, b) {
  17323. return b.dist - a.dist;
  17324. };
  17325. this.dataPoints.sort(sortDepth);
  17326. // draw the datapoints as bars
  17327. var xWidth = this.xBarWidth / 2;
  17328. var yWidth = this.yBarWidth / 2;
  17329. for (i = 0; i < this.dataPoints.length; i++) {
  17330. var point = this.dataPoints[i];
  17331. // determine color
  17332. var hue, color, borderColor;
  17333. if (this.style === Graph3d.STYLE.BARCOLOR ) {
  17334. // calculate the color based on the value
  17335. hue = (1 - (point.point.value - this.valueMin) * this.scale.value) * 240;
  17336. color = this._hsv2rgb(hue, 1, 1);
  17337. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17338. }
  17339. else if (this.style === Graph3d.STYLE.BARSIZE) {
  17340. color = this.colorDot;
  17341. borderColor = this.colorDotBorder;
  17342. }
  17343. else {
  17344. // calculate Hue from the current value. At zMin the hue is 240, at zMax the hue is 0
  17345. hue = (1 - (point.point.z - this.zMin) * this.scale.z / this.verticalRatio) * 240;
  17346. color = this._hsv2rgb(hue, 1, 1);
  17347. borderColor = this._hsv2rgb(hue, 1, 0.8);
  17348. }
  17349. // calculate size for the bar
  17350. if (this.style === Graph3d.STYLE.BARSIZE) {
  17351. xWidth = (this.xBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  17352. yWidth = (this.yBarWidth / 2) * ((point.point.value - this.valueMin) / (this.valueMax - this.valueMin) * 0.8 + 0.2);
  17353. }
  17354. // calculate all corner points
  17355. var me = this;
  17356. var point3d = point.point;
  17357. var top = [
  17358. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, point3d.z)},
  17359. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, point3d.z)},
  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. ];
  17363. var bottom = [
  17364. {point: new Point3d(point3d.x - xWidth, point3d.y - yWidth, this.zMin)},
  17365. {point: new Point3d(point3d.x + xWidth, point3d.y - yWidth, this.zMin)},
  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. ];
  17369. // calculate screen location of the points
  17370. top.forEach(function (obj) {
  17371. obj.screen = me._convert3Dto2D(obj.point);
  17372. });
  17373. bottom.forEach(function (obj) {
  17374. obj.screen = me._convert3Dto2D(obj.point);
  17375. });
  17376. // create five sides, calculate both corner points and center points
  17377. var surfaces = [
  17378. {corners: top, center: Point3d.avg(bottom[0].point, bottom[2].point)},
  17379. {corners: [top[0], top[1], bottom[1], bottom[0]], center: Point3d.avg(bottom[1].point, bottom[0].point)},
  17380. {corners: [top[1], top[2], bottom[2], bottom[1]], center: Point3d.avg(bottom[2].point, bottom[1].point)},
  17381. {corners: [top[2], top[3], bottom[3], bottom[2]], center: Point3d.avg(bottom[3].point, bottom[2].point)},
  17382. {corners: [top[3], top[0], bottom[0], bottom[3]], center: Point3d.avg(bottom[0].point, bottom[3].point)}
  17383. ];
  17384. point.surfaces = surfaces;
  17385. // calculate the distance of each of the surface centers to the camera
  17386. for (j = 0; j < surfaces.length; j++) {
  17387. surface = surfaces[j];
  17388. var transCenter = this._convertPointToTranslation(surface.center);
  17389. surface.dist = this.showPerspective ? transCenter.length() : -transCenter.z;
  17390. // TODO: this dept calculation doesn't work 100% of the cases due to perspective,
  17391. // but the current solution is fast/simple and works in 99.9% of all cases
  17392. // the issue is visible in example 14, with graph.setCameraPosition({horizontal: 2.97, vertical: 0.5, distance: 0.9})
  17393. }
  17394. // order the surfaces by their (translated) depth
  17395. surfaces.sort(function (a, b) {
  17396. var diff = b.dist - a.dist;
  17397. if (diff) return diff;
  17398. // if equal depth, sort the top surface last
  17399. if (a.corners === top) return 1;
  17400. if (b.corners === top) return -1;
  17401. // both are equal
  17402. return 0;
  17403. });
  17404. // draw the ordered surfaces
  17405. ctx.lineWidth = 1;
  17406. ctx.strokeStyle = borderColor;
  17407. ctx.fillStyle = color;
  17408. // NOTE: we start at j=2 instead of j=0 as we don't need to draw the two surfaces at the backside
  17409. for (j = 2; j < surfaces.length; j++) {
  17410. surface = surfaces[j];
  17411. corners = surface.corners;
  17412. ctx.beginPath();
  17413. ctx.moveTo(corners[3].screen.x, corners[3].screen.y);
  17414. ctx.lineTo(corners[0].screen.x, corners[0].screen.y);
  17415. ctx.lineTo(corners[1].screen.x, corners[1].screen.y);
  17416. ctx.lineTo(corners[2].screen.x, corners[2].screen.y);
  17417. ctx.lineTo(corners[3].screen.x, corners[3].screen.y);
  17418. ctx.fill();
  17419. ctx.stroke();
  17420. }
  17421. }
  17422. };
  17423. /**
  17424. * Draw a line through all datapoints.
  17425. * This function can be used when the style is 'line'
  17426. */
  17427. Graph3d.prototype._redrawDataLine = function() {
  17428. var canvas = this.frame.canvas,
  17429. ctx = canvas.getContext('2d'),
  17430. point, i;
  17431. if (this.dataPoints === undefined || this.dataPoints.length <= 0)
  17432. return; // TODO: throw exception?
  17433. // calculate the translations of all points
  17434. for (i = 0; i < this.dataPoints.length; i++) {
  17435. var trans = this._convertPointToTranslation(this.dataPoints[i].point);
  17436. var screen = this._convertTranslationToScreen(trans);
  17437. this.dataPoints[i].trans = trans;
  17438. this.dataPoints[i].screen = screen;
  17439. }
  17440. // start the line
  17441. if (this.dataPoints.length > 0) {
  17442. point = this.dataPoints[0];
  17443. ctx.lineWidth = 1; // TODO: make customizable
  17444. ctx.strokeStyle = 'blue'; // TODO: make customizable
  17445. ctx.beginPath();
  17446. ctx.moveTo(point.screen.x, point.screen.y);
  17447. }
  17448. // draw the datapoints as colored circles
  17449. for (i = 1; i < this.dataPoints.length; i++) {
  17450. point = this.dataPoints[i];
  17451. ctx.lineTo(point.screen.x, point.screen.y);
  17452. }
  17453. // finish the line
  17454. if (this.dataPoints.length > 0) {
  17455. ctx.stroke();
  17456. }
  17457. };
  17458. /**
  17459. * Start a moving operation inside the provided parent element
  17460. * @param {Event} event The event that occurred (required for
  17461. * retrieving the mouse position)
  17462. */
  17463. Graph3d.prototype._onMouseDown = function(event) {
  17464. event = event || window.event;
  17465. // check if mouse is still down (may be up when focus is lost for example
  17466. // in an iframe)
  17467. if (this.leftButtonDown) {
  17468. this._onMouseUp(event);
  17469. }
  17470. // only react on left mouse button down
  17471. this.leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  17472. if (!this.leftButtonDown && !this.touchDown) return;
  17473. // get mouse position (different code for IE and all other browsers)
  17474. this.startMouseX = getMouseX(event);
  17475. this.startMouseY = getMouseY(event);
  17476. this.startStart = new Date(this.start);
  17477. this.startEnd = new Date(this.end);
  17478. this.startArmRotation = this.camera.getArmRotation();
  17479. this.frame.style.cursor = 'move';
  17480. // add event listeners to handle moving the contents
  17481. // we store the function onmousemove and onmouseup in the graph, so we can
  17482. // remove the eventlisteners lateron in the function mouseUp()
  17483. var me = this;
  17484. this.onmousemove = function (event) {me._onMouseMove(event);};
  17485. this.onmouseup = function (event) {me._onMouseUp(event);};
  17486. G3DaddEventListener(document, 'mousemove', me.onmousemove);
  17487. G3DaddEventListener(document, 'mouseup', me.onmouseup);
  17488. G3DpreventDefault(event);
  17489. };
  17490. /**
  17491. * Perform moving operating.
  17492. * This function activated from within the funcion Graph.mouseDown().
  17493. * @param {Event} event Well, eehh, the event
  17494. */
  17495. Graph3d.prototype._onMouseMove = function (event) {
  17496. event = event || window.event;
  17497. // calculate change in mouse position
  17498. var diffX = parseFloat(getMouseX(event)) - this.startMouseX;
  17499. var diffY = parseFloat(getMouseY(event)) - this.startMouseY;
  17500. var horizontalNew = this.startArmRotation.horizontal + diffX / 200;
  17501. var verticalNew = this.startArmRotation.vertical + diffY / 200;
  17502. var snapAngle = 4; // degrees
  17503. var snapValue = Math.sin(snapAngle / 360 * 2 * Math.PI);
  17504. // snap horizontally to nice angles at 0pi, 0.5pi, 1pi, 1.5pi, etc...
  17505. // the -0.001 is to take care that the vertical axis is always drawn at the left front corner
  17506. if (Math.abs(Math.sin(horizontalNew)) < snapValue) {
  17507. horizontalNew = Math.round((horizontalNew / Math.PI)) * Math.PI - 0.001;
  17508. }
  17509. if (Math.abs(Math.cos(horizontalNew)) < snapValue) {
  17510. horizontalNew = (Math.round((horizontalNew/ Math.PI - 0.5)) + 0.5) * Math.PI - 0.001;
  17511. }
  17512. // snap vertically to nice angles
  17513. if (Math.abs(Math.sin(verticalNew)) < snapValue) {
  17514. verticalNew = Math.round((verticalNew / Math.PI)) * Math.PI;
  17515. }
  17516. if (Math.abs(Math.cos(verticalNew)) < snapValue) {
  17517. verticalNew = (Math.round((verticalNew/ Math.PI - 0.5)) + 0.5) * Math.PI;
  17518. }
  17519. this.camera.setArmRotation(horizontalNew, verticalNew);
  17520. this.redraw();
  17521. // fire a cameraPositionChange event
  17522. var parameters = this.getCameraPosition();
  17523. this.emit('cameraPositionChange', parameters);
  17524. G3DpreventDefault(event);
  17525. };
  17526. /**
  17527. * Stop moving operating.
  17528. * This function activated from within the funcion Graph.mouseDown().
  17529. * @param {event} event The event
  17530. */
  17531. Graph3d.prototype._onMouseUp = function (event) {
  17532. this.frame.style.cursor = 'auto';
  17533. this.leftButtonDown = false;
  17534. // remove event listeners here
  17535. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  17536. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  17537. G3DpreventDefault(event);
  17538. };
  17539. /**
  17540. * After having moved the mouse, a tooltip should pop up when the mouse is resting on a data point
  17541. * @param {Event} event A mouse move event
  17542. */
  17543. Graph3d.prototype._onTooltip = function (event) {
  17544. var delay = 300; // ms
  17545. var mouseX = getMouseX(event) - getAbsoluteLeft(this.frame);
  17546. var mouseY = getMouseY(event) - getAbsoluteTop(this.frame);
  17547. if (!this.showTooltip) {
  17548. return;
  17549. }
  17550. if (this.tooltipTimeout) {
  17551. clearTimeout(this.tooltipTimeout);
  17552. }
  17553. // (delayed) display of a tooltip only if no mouse button is down
  17554. if (this.leftButtonDown) {
  17555. this._hideTooltip();
  17556. return;
  17557. }
  17558. if (this.tooltip && this.tooltip.dataPoint) {
  17559. // tooltip is currently visible
  17560. var dataPoint = this._dataPointFromXY(mouseX, mouseY);
  17561. if (dataPoint !== this.tooltip.dataPoint) {
  17562. // datapoint changed
  17563. if (dataPoint) {
  17564. this._showTooltip(dataPoint);
  17565. }
  17566. else {
  17567. this._hideTooltip();
  17568. }
  17569. }
  17570. }
  17571. else {
  17572. // tooltip is currently not visible
  17573. var me = this;
  17574. this.tooltipTimeout = setTimeout(function () {
  17575. me.tooltipTimeout = null;
  17576. // show a tooltip if we have a data point
  17577. var dataPoint = me._dataPointFromXY(mouseX, mouseY);
  17578. if (dataPoint) {
  17579. me._showTooltip(dataPoint);
  17580. }
  17581. }, delay);
  17582. }
  17583. };
  17584. /**
  17585. * Event handler for touchstart event on mobile devices
  17586. */
  17587. Graph3d.prototype._onTouchStart = function(event) {
  17588. this.touchDown = true;
  17589. var me = this;
  17590. this.ontouchmove = function (event) {me._onTouchMove(event);};
  17591. this.ontouchend = function (event) {me._onTouchEnd(event);};
  17592. G3DaddEventListener(document, 'touchmove', me.ontouchmove);
  17593. G3DaddEventListener(document, 'touchend', me.ontouchend);
  17594. this._onMouseDown(event);
  17595. };
  17596. /**
  17597. * Event handler for touchmove event on mobile devices
  17598. */
  17599. Graph3d.prototype._onTouchMove = function(event) {
  17600. this._onMouseMove(event);
  17601. };
  17602. /**
  17603. * Event handler for touchend event on mobile devices
  17604. */
  17605. Graph3d.prototype._onTouchEnd = function(event) {
  17606. this.touchDown = false;
  17607. G3DremoveEventListener(document, 'touchmove', this.ontouchmove);
  17608. G3DremoveEventListener(document, 'touchend', this.ontouchend);
  17609. this._onMouseUp(event);
  17610. };
  17611. /**
  17612. * Event handler for mouse wheel event, used to zoom the graph
  17613. * Code from http://adomas.org/javascript-mouse-wheel/
  17614. * @param {event} event The event
  17615. */
  17616. Graph3d.prototype._onWheel = function(event) {
  17617. if (!event) /* For IE. */
  17618. event = window.event;
  17619. // retrieve delta
  17620. var delta = 0;
  17621. if (event.wheelDelta) { /* IE/Opera. */
  17622. delta = event.wheelDelta/120;
  17623. } else if (event.detail) { /* Mozilla case. */
  17624. // In Mozilla, sign of delta is different than in IE.
  17625. // Also, delta is multiple of 3.
  17626. delta = -event.detail/3;
  17627. }
  17628. // If delta is nonzero, handle it.
  17629. // Basically, delta is now positive if wheel was scrolled up,
  17630. // and negative, if wheel was scrolled down.
  17631. if (delta) {
  17632. var oldLength = this.camera.getArmLength();
  17633. var newLength = oldLength * (1 - delta / 10);
  17634. this.camera.setArmLength(newLength);
  17635. this.redraw();
  17636. this._hideTooltip();
  17637. }
  17638. // fire a cameraPositionChange event
  17639. var parameters = this.getCameraPosition();
  17640. this.emit('cameraPositionChange', parameters);
  17641. // Prevent default actions caused by mouse wheel.
  17642. // That might be ugly, but we handle scrolls somehow
  17643. // anyway, so don't bother here..
  17644. G3DpreventDefault(event);
  17645. };
  17646. /**
  17647. * Test whether a point lies inside given 2D triangle
  17648. * @param {Point2d} point
  17649. * @param {Point2d[]} triangle
  17650. * @return {boolean} Returns true if given point lies inside or on the edge of the triangle
  17651. * @private
  17652. */
  17653. Graph3d.prototype._insideTriangle = function (point, triangle) {
  17654. var a = triangle[0],
  17655. b = triangle[1],
  17656. c = triangle[2];
  17657. function sign (x) {
  17658. return x > 0 ? 1 : x < 0 ? -1 : 0;
  17659. }
  17660. var as = sign((b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x));
  17661. var bs = sign((c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x));
  17662. var cs = sign((a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x));
  17663. // each of the three signs must be either equal to each other or zero
  17664. return (as == 0 || bs == 0 || as == bs) &&
  17665. (bs == 0 || cs == 0 || bs == cs) &&
  17666. (as == 0 || cs == 0 || as == cs);
  17667. };
  17668. /**
  17669. * Find a data point close to given screen position (x, y)
  17670. * @param {Number} x
  17671. * @param {Number} y
  17672. * @return {Object | null} The closest data point or null if not close to any data point
  17673. * @private
  17674. */
  17675. Graph3d.prototype._dataPointFromXY = function (x, y) {
  17676. var i,
  17677. distMax = 100, // px
  17678. dataPoint = null,
  17679. closestDataPoint = null,
  17680. closestDist = null,
  17681. center = new Point2d(x, y);
  17682. if (this.style === Graph3d.STYLE.BAR ||
  17683. this.style === Graph3d.STYLE.BARCOLOR ||
  17684. this.style === Graph3d.STYLE.BARSIZE) {
  17685. // the data points are ordered from far away to closest
  17686. for (i = this.dataPoints.length - 1; i >= 0; i--) {
  17687. dataPoint = this.dataPoints[i];
  17688. var surfaces = dataPoint.surfaces;
  17689. if (surfaces) {
  17690. for (var s = surfaces.length - 1; s >= 0; s--) {
  17691. // split each surface in two triangles, and see if the center point is inside one of these
  17692. var surface = surfaces[s];
  17693. var corners = surface.corners;
  17694. var triangle1 = [corners[0].screen, corners[1].screen, corners[2].screen];
  17695. var triangle2 = [corners[2].screen, corners[3].screen, corners[0].screen];
  17696. if (this._insideTriangle(center, triangle1) ||
  17697. this._insideTriangle(center, triangle2)) {
  17698. // return immediately at the first hit
  17699. return dataPoint;
  17700. }
  17701. }
  17702. }
  17703. }
  17704. }
  17705. else {
  17706. // find the closest data point, using distance to the center of the point on 2d screen
  17707. for (i = 0; i < this.dataPoints.length; i++) {
  17708. dataPoint = this.dataPoints[i];
  17709. var point = dataPoint.screen;
  17710. if (point) {
  17711. var distX = Math.abs(x - point.x);
  17712. var distY = Math.abs(y - point.y);
  17713. var dist = Math.sqrt(distX * distX + distY * distY);
  17714. if ((closestDist === null || dist < closestDist) && dist < distMax) {
  17715. closestDist = dist;
  17716. closestDataPoint = dataPoint;
  17717. }
  17718. }
  17719. }
  17720. }
  17721. return closestDataPoint;
  17722. };
  17723. /**
  17724. * Display a tooltip for given data point
  17725. * @param {Object} dataPoint
  17726. * @private
  17727. */
  17728. Graph3d.prototype._showTooltip = function (dataPoint) {
  17729. var content, line, dot;
  17730. if (!this.tooltip) {
  17731. content = document.createElement('div');
  17732. content.style.position = 'absolute';
  17733. content.style.padding = '10px';
  17734. content.style.border = '1px solid #4d4d4d';
  17735. content.style.color = '#1a1a1a';
  17736. content.style.background = 'rgba(255,255,255,0.7)';
  17737. content.style.borderRadius = '2px';
  17738. content.style.boxShadow = '5px 5px 10px rgba(128,128,128,0.5)';
  17739. line = document.createElement('div');
  17740. line.style.position = 'absolute';
  17741. line.style.height = '40px';
  17742. line.style.width = '0';
  17743. line.style.borderLeft = '1px solid #4d4d4d';
  17744. dot = document.createElement('div');
  17745. dot.style.position = 'absolute';
  17746. dot.style.height = '0';
  17747. dot.style.width = '0';
  17748. dot.style.border = '5px solid #4d4d4d';
  17749. dot.style.borderRadius = '5px';
  17750. this.tooltip = {
  17751. dataPoint: null,
  17752. dom: {
  17753. content: content,
  17754. line: line,
  17755. dot: dot
  17756. }
  17757. };
  17758. }
  17759. else {
  17760. content = this.tooltip.dom.content;
  17761. line = this.tooltip.dom.line;
  17762. dot = this.tooltip.dom.dot;
  17763. }
  17764. this._hideTooltip();
  17765. this.tooltip.dataPoint = dataPoint;
  17766. if (typeof this.showTooltip === 'function') {
  17767. content.innerHTML = this.showTooltip(dataPoint.point);
  17768. }
  17769. else {
  17770. content.innerHTML = '<table>' +
  17771. '<tr><td>x:</td><td>' + dataPoint.point.x + '</td></tr>' +
  17772. '<tr><td>y:</td><td>' + dataPoint.point.y + '</td></tr>' +
  17773. '<tr><td>z:</td><td>' + dataPoint.point.z + '</td></tr>' +
  17774. '</table>';
  17775. }
  17776. content.style.left = '0';
  17777. content.style.top = '0';
  17778. this.frame.appendChild(content);
  17779. this.frame.appendChild(line);
  17780. this.frame.appendChild(dot);
  17781. // calculate sizes
  17782. var contentWidth = content.offsetWidth;
  17783. var contentHeight = content.offsetHeight;
  17784. var lineHeight = line.offsetHeight;
  17785. var dotWidth = dot.offsetWidth;
  17786. var dotHeight = dot.offsetHeight;
  17787. var left = dataPoint.screen.x - contentWidth / 2;
  17788. left = Math.min(Math.max(left, 10), this.frame.clientWidth - 10 - contentWidth);
  17789. line.style.left = dataPoint.screen.x + 'px';
  17790. line.style.top = (dataPoint.screen.y - lineHeight) + 'px';
  17791. content.style.left = left + 'px';
  17792. content.style.top = (dataPoint.screen.y - lineHeight - contentHeight) + 'px';
  17793. dot.style.left = (dataPoint.screen.x - dotWidth / 2) + 'px';
  17794. dot.style.top = (dataPoint.screen.y - dotHeight / 2) + 'px';
  17795. };
  17796. /**
  17797. * Hide the tooltip when displayed
  17798. * @private
  17799. */
  17800. Graph3d.prototype._hideTooltip = function () {
  17801. if (this.tooltip) {
  17802. this.tooltip.dataPoint = null;
  17803. for (var prop in this.tooltip.dom) {
  17804. if (this.tooltip.dom.hasOwnProperty(prop)) {
  17805. var elem = this.tooltip.dom[prop];
  17806. if (elem && elem.parentNode) {
  17807. elem.parentNode.removeChild(elem);
  17808. }
  17809. }
  17810. }
  17811. }
  17812. };
  17813. /**
  17814. * Add and event listener. Works for all browsers
  17815. * @param {Element} element An html element
  17816. * @param {string} action The action, for example 'click',
  17817. * without the prefix 'on'
  17818. * @param {function} listener The callback function to be executed
  17819. * @param {boolean} useCapture
  17820. */
  17821. G3DaddEventListener = function(element, action, listener, useCapture) {
  17822. if (element.addEventListener) {
  17823. if (useCapture === undefined)
  17824. useCapture = false;
  17825. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  17826. action = 'DOMMouseScroll'; // For Firefox
  17827. }
  17828. element.addEventListener(action, listener, useCapture);
  17829. } else {
  17830. element.attachEvent('on' + action, listener); // IE browsers
  17831. }
  17832. };
  17833. /**
  17834. * Remove an event listener from an element
  17835. * @param {Element} element An html dom element
  17836. * @param {string} action The name of the event, for example 'mousedown'
  17837. * @param {function} listener The listener function
  17838. * @param {boolean} useCapture
  17839. */
  17840. G3DremoveEventListener = function(element, action, listener, useCapture) {
  17841. if (element.removeEventListener) {
  17842. // non-IE browsers
  17843. if (useCapture === undefined)
  17844. useCapture = false;
  17845. if (action === 'mousewheel' && navigator.userAgent.indexOf('Firefox') >= 0) {
  17846. action = 'DOMMouseScroll'; // For Firefox
  17847. }
  17848. element.removeEventListener(action, listener, useCapture);
  17849. } else {
  17850. // IE browsers
  17851. element.detachEvent('on' + action, listener);
  17852. }
  17853. };
  17854. /**
  17855. * Stop event propagation
  17856. */
  17857. G3DstopPropagation = function(event) {
  17858. if (!event)
  17859. event = window.event;
  17860. if (event.stopPropagation) {
  17861. event.stopPropagation(); // non-IE browsers
  17862. }
  17863. else {
  17864. event.cancelBubble = true; // IE browsers
  17865. }
  17866. };
  17867. /**
  17868. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  17869. */
  17870. G3DpreventDefault = function (event) {
  17871. if (!event)
  17872. event = window.event;
  17873. if (event.preventDefault) {
  17874. event.preventDefault(); // non-IE browsers
  17875. }
  17876. else {
  17877. event.returnValue = false; // IE browsers
  17878. }
  17879. };
  17880. /**
  17881. * @prototype Point3d
  17882. * @param {Number} x
  17883. * @param {Number} y
  17884. * @param {Number} z
  17885. */
  17886. function Point3d(x, y, z) {
  17887. this.x = x !== undefined ? x : 0;
  17888. this.y = y !== undefined ? y : 0;
  17889. this.z = z !== undefined ? z : 0;
  17890. };
  17891. /**
  17892. * Subtract the two provided points, returns a-b
  17893. * @param {Point3d} a
  17894. * @param {Point3d} b
  17895. * @return {Point3d} a-b
  17896. */
  17897. Point3d.subtract = function(a, b) {
  17898. var sub = new Point3d();
  17899. sub.x = a.x - b.x;
  17900. sub.y = a.y - b.y;
  17901. sub.z = a.z - b.z;
  17902. return sub;
  17903. };
  17904. /**
  17905. * Add the two provided points, returns a+b
  17906. * @param {Point3d} a
  17907. * @param {Point3d} b
  17908. * @return {Point3d} a+b
  17909. */
  17910. Point3d.add = function(a, b) {
  17911. var sum = new Point3d();
  17912. sum.x = a.x + b.x;
  17913. sum.y = a.y + b.y;
  17914. sum.z = a.z + b.z;
  17915. return sum;
  17916. };
  17917. /**
  17918. * Calculate the average of two 3d points
  17919. * @param {Point3d} a
  17920. * @param {Point3d} b
  17921. * @return {Point3d} The average, (a+b)/2
  17922. */
  17923. Point3d.avg = function(a, b) {
  17924. return new Point3d(
  17925. (a.x + b.x) / 2,
  17926. (a.y + b.y) / 2,
  17927. (a.z + b.z) / 2
  17928. );
  17929. };
  17930. /**
  17931. * Calculate the cross product of the two provided points, returns axb
  17932. * Documentation: http://en.wikipedia.org/wiki/Cross_product
  17933. * @param {Point3d} a
  17934. * @param {Point3d} b
  17935. * @return {Point3d} cross product axb
  17936. */
  17937. Point3d.crossProduct = function(a, b) {
  17938. var crossproduct = new Point3d();
  17939. crossproduct.x = a.y * b.z - a.z * b.y;
  17940. crossproduct.y = a.z * b.x - a.x * b.z;
  17941. crossproduct.z = a.x * b.y - a.y * b.x;
  17942. return crossproduct;
  17943. };
  17944. /**
  17945. * Rtrieve the length of the vector (or the distance from this point to the origin
  17946. * @return {Number} length
  17947. */
  17948. Point3d.prototype.length = function() {
  17949. return Math.sqrt(
  17950. this.x * this.x +
  17951. this.y * this.y +
  17952. this.z * this.z
  17953. );
  17954. };
  17955. /**
  17956. * @prototype Point2d
  17957. */
  17958. Point2d = function (x, y) {
  17959. this.x = x !== undefined ? x : 0;
  17960. this.y = y !== undefined ? y : 0;
  17961. };
  17962. /**
  17963. * @class Filter
  17964. *
  17965. * @param {DataSet} data The google data table
  17966. * @param {Number} column The index of the column to be filtered
  17967. * @param {Graph} graph The graph
  17968. */
  17969. function Filter (data, column, graph) {
  17970. this.data = data;
  17971. this.column = column;
  17972. this.graph = graph; // the parent graph
  17973. this.index = undefined;
  17974. this.value = undefined;
  17975. // read all distinct values and select the first one
  17976. this.values = graph.getDistinctValues(data.get(), this.column);
  17977. // sort both numeric and string values correctly
  17978. this.values.sort(function (a, b) {
  17979. return a > b ? 1 : a < b ? -1 : 0;
  17980. });
  17981. if (this.values.length > 0) {
  17982. this.selectValue(0);
  17983. }
  17984. // create an array with the filtered datapoints. this will be loaded afterwards
  17985. this.dataPoints = [];
  17986. this.loaded = false;
  17987. this.onLoadCallback = undefined;
  17988. if (graph.animationPreload) {
  17989. this.loaded = false;
  17990. this.loadInBackground();
  17991. }
  17992. else {
  17993. this.loaded = true;
  17994. }
  17995. };
  17996. /**
  17997. * Return the label
  17998. * @return {string} label
  17999. */
  18000. Filter.prototype.isLoaded = function() {
  18001. return this.loaded;
  18002. };
  18003. /**
  18004. * Return the loaded progress
  18005. * @return {Number} percentage between 0 and 100
  18006. */
  18007. Filter.prototype.getLoadedProgress = function() {
  18008. var len = this.values.length;
  18009. var i = 0;
  18010. while (this.dataPoints[i]) {
  18011. i++;
  18012. }
  18013. return Math.round(i / len * 100);
  18014. };
  18015. /**
  18016. * Return the label
  18017. * @return {string} label
  18018. */
  18019. Filter.prototype.getLabel = function() {
  18020. return this.graph.filterLabel;
  18021. };
  18022. /**
  18023. * Return the columnIndex of the filter
  18024. * @return {Number} columnIndex
  18025. */
  18026. Filter.prototype.getColumn = function() {
  18027. return this.column;
  18028. };
  18029. /**
  18030. * Return the currently selected value. Returns undefined if there is no selection
  18031. * @return {*} value
  18032. */
  18033. Filter.prototype.getSelectedValue = function() {
  18034. if (this.index === undefined)
  18035. return undefined;
  18036. return this.values[this.index];
  18037. };
  18038. /**
  18039. * Retrieve all values of the filter
  18040. * @return {Array} values
  18041. */
  18042. Filter.prototype.getValues = function() {
  18043. return this.values;
  18044. };
  18045. /**
  18046. * Retrieve one value of the filter
  18047. * @param {Number} index
  18048. * @return {*} value
  18049. */
  18050. Filter.prototype.getValue = function(index) {
  18051. if (index >= this.values.length)
  18052. throw 'Error: index out of range';
  18053. return this.values[index];
  18054. };
  18055. /**
  18056. * Retrieve the (filtered) dataPoints for the currently selected filter index
  18057. * @param {Number} [index] (optional)
  18058. * @return {Array} dataPoints
  18059. */
  18060. Filter.prototype._getDataPoints = function(index) {
  18061. if (index === undefined)
  18062. index = this.index;
  18063. if (index === undefined)
  18064. return [];
  18065. var dataPoints;
  18066. if (this.dataPoints[index]) {
  18067. dataPoints = this.dataPoints[index];
  18068. }
  18069. else {
  18070. var f = {};
  18071. f.column = this.column;
  18072. f.value = this.values[index];
  18073. var dataView = new DataView(this.data,{filter: function (item) {return (item[f.column] == f.value);}}).get();
  18074. dataPoints = this.graph._getDataPoints(dataView);
  18075. this.dataPoints[index] = dataPoints;
  18076. }
  18077. return dataPoints;
  18078. };
  18079. /**
  18080. * Set a callback function when the filter is fully loaded.
  18081. */
  18082. Filter.prototype.setOnLoadCallback = function(callback) {
  18083. this.onLoadCallback = callback;
  18084. };
  18085. /**
  18086. * Add a value to the list with available values for this filter
  18087. * No double entries will be created.
  18088. * @param {Number} index
  18089. */
  18090. Filter.prototype.selectValue = function(index) {
  18091. if (index >= this.values.length)
  18092. throw 'Error: index out of range';
  18093. this.index = index;
  18094. this.value = this.values[index];
  18095. };
  18096. /**
  18097. * Load all filtered rows in the background one by one
  18098. * Start this method without providing an index!
  18099. */
  18100. Filter.prototype.loadInBackground = function(index) {
  18101. if (index === undefined)
  18102. index = 0;
  18103. var frame = this.graph.frame;
  18104. if (index < this.values.length) {
  18105. var dataPointsTemp = this._getDataPoints(index);
  18106. //this.graph.redrawInfo(); // TODO: not neat
  18107. // create a progress box
  18108. if (frame.progress === undefined) {
  18109. frame.progress = document.createElement('DIV');
  18110. frame.progress.style.position = 'absolute';
  18111. frame.progress.style.color = 'gray';
  18112. frame.appendChild(frame.progress);
  18113. }
  18114. var progress = this.getLoadedProgress();
  18115. frame.progress.innerHTML = 'Loading animation... ' + progress + '%';
  18116. // TODO: this is no nice solution...
  18117. frame.progress.style.bottom = Graph3d.px(60); // TODO: use height of slider
  18118. frame.progress.style.left = Graph3d.px(10);
  18119. var me = this;
  18120. setTimeout(function() {me.loadInBackground(index+1);}, 10);
  18121. this.loaded = false;
  18122. }
  18123. else {
  18124. this.loaded = true;
  18125. // remove the progress box
  18126. if (frame.progress !== undefined) {
  18127. frame.removeChild(frame.progress);
  18128. frame.progress = undefined;
  18129. }
  18130. if (this.onLoadCallback)
  18131. this.onLoadCallback();
  18132. }
  18133. };
  18134. /**
  18135. * @prototype StepNumber
  18136. * The class StepNumber is an iterator for Numbers. You provide a start and end
  18137. * value, and a best step size. StepNumber itself rounds to fixed values and
  18138. * a finds the step that best fits the provided step.
  18139. *
  18140. * If prettyStep is true, the step size is chosen as close as possible to the
  18141. * provided step, but being a round value like 1, 2, 5, 10, 20, 50, ....
  18142. *
  18143. * Example usage:
  18144. * var step = new StepNumber(0, 10, 2.5, true);
  18145. * step.start();
  18146. * while (!step.end()) {
  18147. * alert(step.getCurrent());
  18148. * step.next();
  18149. * }
  18150. *
  18151. * Version: 1.0
  18152. *
  18153. * @param {Number} start The start value
  18154. * @param {Number} end The end value
  18155. * @param {Number} step Optional. Step size. Must be a positive value.
  18156. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  18157. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  18158. */
  18159. StepNumber = function (start, end, step, prettyStep) {
  18160. // set default values
  18161. this._start = 0;
  18162. this._end = 0;
  18163. this._step = 1;
  18164. this.prettyStep = true;
  18165. this.precision = 5;
  18166. this._current = 0;
  18167. this.setRange(start, end, step, prettyStep);
  18168. };
  18169. /**
  18170. * Set a new range: start, end and step.
  18171. *
  18172. * @param {Number} start The start value
  18173. * @param {Number} end The end value
  18174. * @param {Number} step Optional. Step size. Must be a positive value.
  18175. * @param {boolean} prettyStep Optional. If true, the step size is rounded
  18176. * To a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  18177. */
  18178. StepNumber.prototype.setRange = function(start, end, step, prettyStep) {
  18179. this._start = start ? start : 0;
  18180. this._end = end ? end : 0;
  18181. this.setStep(step, prettyStep);
  18182. };
  18183. /**
  18184. * Set a new step size
  18185. * @param {Number} step New step size. Must be a positive value
  18186. * @param {boolean} prettyStep Optional. If true, the provided step is rounded
  18187. * to a pretty step size (like 1, 2, 5, 10, 20, 50, ...)
  18188. */
  18189. StepNumber.prototype.setStep = function(step, prettyStep) {
  18190. if (step === undefined || step <= 0)
  18191. return;
  18192. if (prettyStep !== undefined)
  18193. this.prettyStep = prettyStep;
  18194. if (this.prettyStep === true)
  18195. this._step = StepNumber.calculatePrettyStep(step);
  18196. else
  18197. this._step = step;
  18198. };
  18199. /**
  18200. * Calculate a nice step size, closest to the desired step size.
  18201. * Returns a value in one of the ranges 1*10^n, 2*10^n, or 5*10^n, where n is an
  18202. * integer Number. For example 1, 2, 5, 10, 20, 50, etc...
  18203. * @param {Number} step Desired step size
  18204. * @return {Number} Nice step size
  18205. */
  18206. StepNumber.calculatePrettyStep = function (step) {
  18207. var log10 = function (x) {return Math.log(x) / Math.LN10;};
  18208. // try three steps (multiple of 1, 2, or 5
  18209. var step1 = Math.pow(10, Math.round(log10(step))),
  18210. step2 = 2 * Math.pow(10, Math.round(log10(step / 2))),
  18211. step5 = 5 * Math.pow(10, Math.round(log10(step / 5)));
  18212. // choose the best step (closest to minimum step)
  18213. var prettyStep = step1;
  18214. if (Math.abs(step2 - step) <= Math.abs(prettyStep - step)) prettyStep = step2;
  18215. if (Math.abs(step5 - step) <= Math.abs(prettyStep - step)) prettyStep = step5;
  18216. // for safety
  18217. if (prettyStep <= 0) {
  18218. prettyStep = 1;
  18219. }
  18220. return prettyStep;
  18221. };
  18222. /**
  18223. * returns the current value of the step
  18224. * @return {Number} current value
  18225. */
  18226. StepNumber.prototype.getCurrent = function () {
  18227. return parseFloat(this._current.toPrecision(this.precision));
  18228. };
  18229. /**
  18230. * returns the current step size
  18231. * @return {Number} current step size
  18232. */
  18233. StepNumber.prototype.getStep = function () {
  18234. return this._step;
  18235. };
  18236. /**
  18237. * Set the current value to the largest value smaller than start, which
  18238. * is a multiple of the step size
  18239. */
  18240. StepNumber.prototype.start = function() {
  18241. this._current = this._start - this._start % this._step;
  18242. };
  18243. /**
  18244. * Do a step, add the step size to the current value
  18245. */
  18246. StepNumber.prototype.next = function () {
  18247. this._current += this._step;
  18248. };
  18249. /**
  18250. * Returns true whether the end is reached
  18251. * @return {boolean} True if the current value has passed the end value.
  18252. */
  18253. StepNumber.prototype.end = function () {
  18254. return (this._current > this._end);
  18255. };
  18256. /**
  18257. * @constructor Slider
  18258. *
  18259. * An html slider control with start/stop/prev/next buttons
  18260. * @param {Element} container The element where the slider will be created
  18261. * @param {Object} options Available options:
  18262. * {boolean} visible If true (default) the
  18263. * slider is visible.
  18264. */
  18265. function Slider(container, options) {
  18266. if (container === undefined) {
  18267. throw 'Error: No container element defined';
  18268. }
  18269. this.container = container;
  18270. this.visible = (options && options.visible != undefined) ? options.visible : true;
  18271. if (this.visible) {
  18272. this.frame = document.createElement('DIV');
  18273. //this.frame.style.backgroundColor = '#E5E5E5';
  18274. this.frame.style.width = '100%';
  18275. this.frame.style.position = 'relative';
  18276. this.container.appendChild(this.frame);
  18277. this.frame.prev = document.createElement('INPUT');
  18278. this.frame.prev.type = 'BUTTON';
  18279. this.frame.prev.value = 'Prev';
  18280. this.frame.appendChild(this.frame.prev);
  18281. this.frame.play = document.createElement('INPUT');
  18282. this.frame.play.type = 'BUTTON';
  18283. this.frame.play.value = 'Play';
  18284. this.frame.appendChild(this.frame.play);
  18285. this.frame.next = document.createElement('INPUT');
  18286. this.frame.next.type = 'BUTTON';
  18287. this.frame.next.value = 'Next';
  18288. this.frame.appendChild(this.frame.next);
  18289. this.frame.bar = document.createElement('INPUT');
  18290. this.frame.bar.type = 'BUTTON';
  18291. this.frame.bar.style.position = 'absolute';
  18292. this.frame.bar.style.border = '1px solid red';
  18293. this.frame.bar.style.width = '100px';
  18294. this.frame.bar.style.height = '6px';
  18295. this.frame.bar.style.borderRadius = '2px';
  18296. this.frame.bar.style.MozBorderRadius = '2px';
  18297. this.frame.bar.style.border = '1px solid #7F7F7F';
  18298. this.frame.bar.style.backgroundColor = '#E5E5E5';
  18299. this.frame.appendChild(this.frame.bar);
  18300. this.frame.slide = document.createElement('INPUT');
  18301. this.frame.slide.type = 'BUTTON';
  18302. this.frame.slide.style.margin = '0px';
  18303. this.frame.slide.value = ' ';
  18304. this.frame.slide.style.position = 'relative';
  18305. this.frame.slide.style.left = '-100px';
  18306. this.frame.appendChild(this.frame.slide);
  18307. // create events
  18308. var me = this;
  18309. this.frame.slide.onmousedown = function (event) {me._onMouseDown(event);};
  18310. this.frame.prev.onclick = function (event) {me.prev(event);};
  18311. this.frame.play.onclick = function (event) {me.togglePlay(event);};
  18312. this.frame.next.onclick = function (event) {me.next(event);};
  18313. }
  18314. this.onChangeCallback = undefined;
  18315. this.values = [];
  18316. this.index = undefined;
  18317. this.playTimeout = undefined;
  18318. this.playInterval = 1000; // milliseconds
  18319. this.playLoop = true;
  18320. };
  18321. /**
  18322. * Select the previous index
  18323. */
  18324. Slider.prototype.prev = function() {
  18325. var index = this.getIndex();
  18326. if (index > 0) {
  18327. index--;
  18328. this.setIndex(index);
  18329. }
  18330. };
  18331. /**
  18332. * Select the next index
  18333. */
  18334. Slider.prototype.next = function() {
  18335. var index = this.getIndex();
  18336. if (index < this.values.length - 1) {
  18337. index++;
  18338. this.setIndex(index);
  18339. }
  18340. };
  18341. /**
  18342. * Select the next index
  18343. */
  18344. Slider.prototype.playNext = function() {
  18345. var start = new Date();
  18346. var index = this.getIndex();
  18347. if (index < this.values.length - 1) {
  18348. index++;
  18349. this.setIndex(index);
  18350. }
  18351. else if (this.playLoop) {
  18352. // jump to the start
  18353. index = 0;
  18354. this.setIndex(index);
  18355. }
  18356. var end = new Date();
  18357. var diff = (end - start);
  18358. // calculate how much time it to to set the index and to execute the callback
  18359. // function.
  18360. var interval = Math.max(this.playInterval - diff, 0);
  18361. // document.title = diff // TODO: cleanup
  18362. var me = this;
  18363. this.playTimeout = setTimeout(function() {me.playNext();}, interval);
  18364. };
  18365. /**
  18366. * Toggle start or stop playing
  18367. */
  18368. Slider.prototype.togglePlay = function() {
  18369. if (this.playTimeout === undefined) {
  18370. this.play();
  18371. } else {
  18372. this.stop();
  18373. }
  18374. };
  18375. /**
  18376. * Start playing
  18377. */
  18378. Slider.prototype.play = function() {
  18379. // Test whether already playing
  18380. if (this.playTimeout) return;
  18381. this.playNext();
  18382. if (this.frame) {
  18383. this.frame.play.value = 'Stop';
  18384. }
  18385. };
  18386. /**
  18387. * Stop playing
  18388. */
  18389. Slider.prototype.stop = function() {
  18390. clearInterval(this.playTimeout);
  18391. this.playTimeout = undefined;
  18392. if (this.frame) {
  18393. this.frame.play.value = 'Play';
  18394. }
  18395. };
  18396. /**
  18397. * Set a callback function which will be triggered when the value of the
  18398. * slider bar has changed.
  18399. */
  18400. Slider.prototype.setOnChangeCallback = function(callback) {
  18401. this.onChangeCallback = callback;
  18402. };
  18403. /**
  18404. * Set the interval for playing the list
  18405. * @param {Number} interval The interval in milliseconds
  18406. */
  18407. Slider.prototype.setPlayInterval = function(interval) {
  18408. this.playInterval = interval;
  18409. };
  18410. /**
  18411. * Retrieve the current play interval
  18412. * @return {Number} interval The interval in milliseconds
  18413. */
  18414. Slider.prototype.getPlayInterval = function(interval) {
  18415. return this.playInterval;
  18416. };
  18417. /**
  18418. * Set looping on or off
  18419. * @pararm {boolean} doLoop If true, the slider will jump to the start when
  18420. * the end is passed, and will jump to the end
  18421. * when the start is passed.
  18422. */
  18423. Slider.prototype.setPlayLoop = function(doLoop) {
  18424. this.playLoop = doLoop;
  18425. };
  18426. /**
  18427. * Execute the onchange callback function
  18428. */
  18429. Slider.prototype.onChange = function() {
  18430. if (this.onChangeCallback !== undefined) {
  18431. this.onChangeCallback();
  18432. }
  18433. };
  18434. /**
  18435. * redraw the slider on the correct place
  18436. */
  18437. Slider.prototype.redraw = function() {
  18438. if (this.frame) {
  18439. // resize the bar
  18440. this.frame.bar.style.top = (this.frame.clientHeight/2 -
  18441. this.frame.bar.offsetHeight/2) + 'px';
  18442. this.frame.bar.style.width = (this.frame.clientWidth -
  18443. this.frame.prev.clientWidth -
  18444. this.frame.play.clientWidth -
  18445. this.frame.next.clientWidth - 30) + 'px';
  18446. // position the slider button
  18447. var left = this.indexToLeft(this.index);
  18448. this.frame.slide.style.left = (left) + 'px';
  18449. }
  18450. };
  18451. /**
  18452. * Set the list with values for the slider
  18453. * @param {Array} values A javascript array with values (any type)
  18454. */
  18455. Slider.prototype.setValues = function(values) {
  18456. this.values = values;
  18457. if (this.values.length > 0)
  18458. this.setIndex(0);
  18459. else
  18460. this.index = undefined;
  18461. };
  18462. /**
  18463. * Select a value by its index
  18464. * @param {Number} index
  18465. */
  18466. Slider.prototype.setIndex = function(index) {
  18467. if (index < this.values.length) {
  18468. this.index = index;
  18469. this.redraw();
  18470. this.onChange();
  18471. }
  18472. else {
  18473. throw 'Error: index out of range';
  18474. }
  18475. };
  18476. /**
  18477. * retrieve the index of the currently selected vaue
  18478. * @return {Number} index
  18479. */
  18480. Slider.prototype.getIndex = function() {
  18481. return this.index;
  18482. };
  18483. /**
  18484. * retrieve the currently selected value
  18485. * @return {*} value
  18486. */
  18487. Slider.prototype.get = function() {
  18488. return this.values[this.index];
  18489. };
  18490. Slider.prototype._onMouseDown = function(event) {
  18491. // only react on left mouse button down
  18492. var leftButtonDown = event.which ? (event.which === 1) : (event.button === 1);
  18493. if (!leftButtonDown) return;
  18494. this.startClientX = event.clientX;
  18495. this.startSlideX = parseFloat(this.frame.slide.style.left);
  18496. this.frame.style.cursor = 'move';
  18497. // add event listeners to handle moving the contents
  18498. // we store the function onmousemove and onmouseup in the graph, so we can
  18499. // remove the eventlisteners lateron in the function mouseUp()
  18500. var me = this;
  18501. this.onmousemove = function (event) {me._onMouseMove(event);};
  18502. this.onmouseup = function (event) {me._onMouseUp(event);};
  18503. G3DaddEventListener(document, 'mousemove', this.onmousemove);
  18504. G3DaddEventListener(document, 'mouseup', this.onmouseup);
  18505. G3DpreventDefault(event);
  18506. };
  18507. Slider.prototype.leftToIndex = function (left) {
  18508. var width = parseFloat(this.frame.bar.style.width) -
  18509. this.frame.slide.clientWidth - 10;
  18510. var x = left - 3;
  18511. var index = Math.round(x / width * (this.values.length-1));
  18512. if (index < 0) index = 0;
  18513. if (index > this.values.length-1) index = this.values.length-1;
  18514. return index;
  18515. };
  18516. Slider.prototype.indexToLeft = function (index) {
  18517. var width = parseFloat(this.frame.bar.style.width) -
  18518. this.frame.slide.clientWidth - 10;
  18519. var x = index / (this.values.length-1) * width;
  18520. var left = x + 3;
  18521. return left;
  18522. };
  18523. Slider.prototype._onMouseMove = function (event) {
  18524. var diff = event.clientX - this.startClientX;
  18525. var x = this.startSlideX + diff;
  18526. var index = this.leftToIndex(x);
  18527. this.setIndex(index);
  18528. G3DpreventDefault();
  18529. };
  18530. Slider.prototype._onMouseUp = function (event) {
  18531. this.frame.style.cursor = 'auto';
  18532. // remove event listeners
  18533. G3DremoveEventListener(document, 'mousemove', this.onmousemove);
  18534. G3DremoveEventListener(document, 'mouseup', this.onmouseup);
  18535. G3DpreventDefault();
  18536. };
  18537. /**--------------------------------------------------------------------------**/
  18538. /**
  18539. * Retrieve the absolute left value of a DOM element
  18540. * @param {Element} elem A dom element, for example a div
  18541. * @return {Number} left The absolute left position of this element
  18542. * in the browser page.
  18543. */
  18544. getAbsoluteLeft = function(elem) {
  18545. var left = 0;
  18546. while( elem !== null ) {
  18547. left += elem.offsetLeft;
  18548. left -= elem.scrollLeft;
  18549. elem = elem.offsetParent;
  18550. }
  18551. return left;
  18552. };
  18553. /**
  18554. * Retrieve the absolute top value of a DOM element
  18555. * @param {Element} elem A dom element, for example a div
  18556. * @return {Number} top The absolute top position of this element
  18557. * in the browser page.
  18558. */
  18559. getAbsoluteTop = function(elem) {
  18560. var top = 0;
  18561. while( elem !== null ) {
  18562. top += elem.offsetTop;
  18563. top -= elem.scrollTop;
  18564. elem = elem.offsetParent;
  18565. }
  18566. return top;
  18567. };
  18568. /**
  18569. * Get the horizontal mouse position from a mouse event
  18570. * @param {Event} event
  18571. * @return {Number} mouse x
  18572. */
  18573. getMouseX = function(event) {
  18574. if ('clientX' in event) return event.clientX;
  18575. return event.targetTouches[0] && event.targetTouches[0].clientX || 0;
  18576. };
  18577. /**
  18578. * Get the vertical mouse position from a mouse event
  18579. * @param {Event} event
  18580. * @return {Number} mouse y
  18581. */
  18582. getMouseY = function(event) {
  18583. if ('clientY' in event) return event.clientY;
  18584. return event.targetTouches[0] && event.targetTouches[0].clientY || 0;
  18585. };
  18586. /**
  18587. * vis.js module exports
  18588. */
  18589. var vis = {
  18590. util: util,
  18591. moment: moment,
  18592. DataSet: DataSet,
  18593. DataView: DataView,
  18594. Range: Range,
  18595. stack: stack,
  18596. TimeStep: TimeStep,
  18597. components: {
  18598. items: {
  18599. Item: Item,
  18600. ItemBox: ItemBox,
  18601. ItemPoint: ItemPoint,
  18602. ItemRange: ItemRange
  18603. },
  18604. Component: Component,
  18605. Panel: Panel,
  18606. RootPanel: RootPanel,
  18607. ItemSet: ItemSet,
  18608. TimeAxis: TimeAxis
  18609. },
  18610. graph: {
  18611. Node: Node,
  18612. Edge: Edge,
  18613. Popup: Popup,
  18614. Groups: Groups,
  18615. Images: Images
  18616. },
  18617. Timeline: Timeline,
  18618. Graph: Graph,
  18619. Graph3d: Graph3d
  18620. };
  18621. /**
  18622. * CommonJS module exports
  18623. */
  18624. if (typeof exports !== 'undefined') {
  18625. exports = vis;
  18626. }
  18627. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  18628. module.exports = vis;
  18629. }
  18630. /**
  18631. * AMD module exports
  18632. */
  18633. if (typeof(define) === 'function') {
  18634. define(function () {
  18635. return vis;
  18636. });
  18637. }
  18638. /**
  18639. * Window exports
  18640. */
  18641. if (typeof window !== 'undefined') {
  18642. // attach the module to the window, load as a regular javascript file
  18643. window['vis'] = vis;
  18644. }
  18645. },{"emitter-component":2,"hammerjs":3,"moment":4,"mousetrap":5}],2:[function(require,module,exports){
  18646. /**
  18647. * Expose `Emitter`.
  18648. */
  18649. module.exports = Emitter;
  18650. /**
  18651. * Initialize a new `Emitter`.
  18652. *
  18653. * @api public
  18654. */
  18655. function Emitter(obj) {
  18656. if (obj) return mixin(obj);
  18657. };
  18658. /**
  18659. * Mixin the emitter properties.
  18660. *
  18661. * @param {Object} obj
  18662. * @return {Object}
  18663. * @api private
  18664. */
  18665. function mixin(obj) {
  18666. for (var key in Emitter.prototype) {
  18667. obj[key] = Emitter.prototype[key];
  18668. }
  18669. return obj;
  18670. }
  18671. /**
  18672. * Listen on the given `event` with `fn`.
  18673. *
  18674. * @param {String} event
  18675. * @param {Function} fn
  18676. * @return {Emitter}
  18677. * @api public
  18678. */
  18679. Emitter.prototype.on =
  18680. Emitter.prototype.addEventListener = function(event, fn){
  18681. this._callbacks = this._callbacks || {};
  18682. (this._callbacks[event] = this._callbacks[event] || [])
  18683. .push(fn);
  18684. return this;
  18685. };
  18686. /**
  18687. * Adds an `event` listener that will be invoked a single
  18688. * time then automatically removed.
  18689. *
  18690. * @param {String} event
  18691. * @param {Function} fn
  18692. * @return {Emitter}
  18693. * @api public
  18694. */
  18695. Emitter.prototype.once = function(event, fn){
  18696. var self = this;
  18697. this._callbacks = this._callbacks || {};
  18698. function on() {
  18699. self.off(event, on);
  18700. fn.apply(this, arguments);
  18701. }
  18702. on.fn = fn;
  18703. this.on(event, on);
  18704. return this;
  18705. };
  18706. /**
  18707. * Remove the given callback for `event` or all
  18708. * registered callbacks.
  18709. *
  18710. * @param {String} event
  18711. * @param {Function} fn
  18712. * @return {Emitter}
  18713. * @api public
  18714. */
  18715. Emitter.prototype.off =
  18716. Emitter.prototype.removeListener =
  18717. Emitter.prototype.removeAllListeners =
  18718. Emitter.prototype.removeEventListener = function(event, fn){
  18719. this._callbacks = this._callbacks || {};
  18720. // all
  18721. if (0 == arguments.length) {
  18722. this._callbacks = {};
  18723. return this;
  18724. }
  18725. // specific event
  18726. var callbacks = this._callbacks[event];
  18727. if (!callbacks) return this;
  18728. // remove all handlers
  18729. if (1 == arguments.length) {
  18730. delete this._callbacks[event];
  18731. return this;
  18732. }
  18733. // remove specific handler
  18734. var cb;
  18735. for (var i = 0; i < callbacks.length; i++) {
  18736. cb = callbacks[i];
  18737. if (cb === fn || cb.fn === fn) {
  18738. callbacks.splice(i, 1);
  18739. break;
  18740. }
  18741. }
  18742. return this;
  18743. };
  18744. /**
  18745. * Emit `event` with the given args.
  18746. *
  18747. * @param {String} event
  18748. * @param {Mixed} ...
  18749. * @return {Emitter}
  18750. */
  18751. Emitter.prototype.emit = function(event){
  18752. this._callbacks = this._callbacks || {};
  18753. var args = [].slice.call(arguments, 1)
  18754. , callbacks = this._callbacks[event];
  18755. if (callbacks) {
  18756. callbacks = callbacks.slice(0);
  18757. for (var i = 0, len = callbacks.length; i < len; ++i) {
  18758. callbacks[i].apply(this, args);
  18759. }
  18760. }
  18761. return this;
  18762. };
  18763. /**
  18764. * Return array of callbacks for `event`.
  18765. *
  18766. * @param {String} event
  18767. * @return {Array}
  18768. * @api public
  18769. */
  18770. Emitter.prototype.listeners = function(event){
  18771. this._callbacks = this._callbacks || {};
  18772. return this._callbacks[event] || [];
  18773. };
  18774. /**
  18775. * Check if this emitter has `event` handlers.
  18776. *
  18777. * @param {String} event
  18778. * @return {Boolean}
  18779. * @api public
  18780. */
  18781. Emitter.prototype.hasListeners = function(event){
  18782. return !! this.listeners(event).length;
  18783. };
  18784. },{}],3:[function(require,module,exports){
  18785. /*! Hammer.JS - v1.0.5 - 2013-04-07
  18786. * http://eightmedia.github.com/hammer.js
  18787. *
  18788. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  18789. * Licensed under the MIT license */
  18790. (function(window, undefined) {
  18791. 'use strict';
  18792. /**
  18793. * Hammer
  18794. * use this to create instances
  18795. * @param {HTMLElement} element
  18796. * @param {Object} options
  18797. * @returns {Hammer.Instance}
  18798. * @constructor
  18799. */
  18800. var Hammer = function(element, options) {
  18801. return new Hammer.Instance(element, options || {});
  18802. };
  18803. // default settings
  18804. Hammer.defaults = {
  18805. // add styles and attributes to the element to prevent the browser from doing
  18806. // its native behavior. this doesnt prevent the scrolling, but cancels
  18807. // the contextmenu, tap highlighting etc
  18808. // set to false to disable this
  18809. stop_browser_behavior: {
  18810. // this also triggers onselectstart=false for IE
  18811. userSelect: 'none',
  18812. // this makes the element blocking in IE10 >, you could experiment with the value
  18813. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  18814. touchAction: 'none',
  18815. touchCallout: 'none',
  18816. contentZooming: 'none',
  18817. userDrag: 'none',
  18818. tapHighlightColor: 'rgba(0,0,0,0)'
  18819. }
  18820. // more settings are defined per gesture at gestures.js
  18821. };
  18822. // detect touchevents
  18823. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  18824. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  18825. // dont use mouseevents on mobile devices
  18826. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  18827. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  18828. // eventtypes per touchevent (start, move, end)
  18829. // are filled by Hammer.event.determineEventTypes on setup
  18830. Hammer.EVENT_TYPES = {};
  18831. // direction defines
  18832. Hammer.DIRECTION_DOWN = 'down';
  18833. Hammer.DIRECTION_LEFT = 'left';
  18834. Hammer.DIRECTION_UP = 'up';
  18835. Hammer.DIRECTION_RIGHT = 'right';
  18836. // pointer type
  18837. Hammer.POINTER_MOUSE = 'mouse';
  18838. Hammer.POINTER_TOUCH = 'touch';
  18839. Hammer.POINTER_PEN = 'pen';
  18840. // touch event defines
  18841. Hammer.EVENT_START = 'start';
  18842. Hammer.EVENT_MOVE = 'move';
  18843. Hammer.EVENT_END = 'end';
  18844. // hammer document where the base events are added at
  18845. Hammer.DOCUMENT = document;
  18846. // plugins namespace
  18847. Hammer.plugins = {};
  18848. // if the window events are set...
  18849. Hammer.READY = false;
  18850. /**
  18851. * setup events to detect gestures on the document
  18852. */
  18853. function setup() {
  18854. if(Hammer.READY) {
  18855. return;
  18856. }
  18857. // find what eventtypes we add listeners to
  18858. Hammer.event.determineEventTypes();
  18859. // Register all gestures inside Hammer.gestures
  18860. for(var name in Hammer.gestures) {
  18861. if(Hammer.gestures.hasOwnProperty(name)) {
  18862. Hammer.detection.register(Hammer.gestures[name]);
  18863. }
  18864. }
  18865. // Add touch events on the document
  18866. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  18867. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  18868. // Hammer is ready...!
  18869. Hammer.READY = true;
  18870. }
  18871. /**
  18872. * create new hammer instance
  18873. * all methods should return the instance itself, so it is chainable.
  18874. * @param {HTMLElement} element
  18875. * @param {Object} [options={}]
  18876. * @returns {Hammer.Instance}
  18877. * @constructor
  18878. */
  18879. Hammer.Instance = function(element, options) {
  18880. var self = this;
  18881. // setup HammerJS window events and register all gestures
  18882. // this also sets up the default options
  18883. setup();
  18884. this.element = element;
  18885. // start/stop detection option
  18886. this.enabled = true;
  18887. // merge options
  18888. this.options = Hammer.utils.extend(
  18889. Hammer.utils.extend({}, Hammer.defaults),
  18890. options || {});
  18891. // add some css to the element to prevent the browser from doing its native behavoir
  18892. if(this.options.stop_browser_behavior) {
  18893. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  18894. }
  18895. // start detection on touchstart
  18896. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  18897. if(self.enabled) {
  18898. Hammer.detection.startDetect(self, ev);
  18899. }
  18900. });
  18901. // return instance
  18902. return this;
  18903. };
  18904. Hammer.Instance.prototype = {
  18905. /**
  18906. * bind events to the instance
  18907. * @param {String} gesture
  18908. * @param {Function} handler
  18909. * @returns {Hammer.Instance}
  18910. */
  18911. on: function onEvent(gesture, handler){
  18912. var gestures = gesture.split(' ');
  18913. for(var t=0; t<gestures.length; t++) {
  18914. this.element.addEventListener(gestures[t], handler, false);
  18915. }
  18916. return this;
  18917. },
  18918. /**
  18919. * unbind events to the instance
  18920. * @param {String} gesture
  18921. * @param {Function} handler
  18922. * @returns {Hammer.Instance}
  18923. */
  18924. off: function offEvent(gesture, handler){
  18925. var gestures = gesture.split(' ');
  18926. for(var t=0; t<gestures.length; t++) {
  18927. this.element.removeEventListener(gestures[t], handler, false);
  18928. }
  18929. return this;
  18930. },
  18931. /**
  18932. * trigger gesture event
  18933. * @param {String} gesture
  18934. * @param {Object} eventData
  18935. * @returns {Hammer.Instance}
  18936. */
  18937. trigger: function triggerEvent(gesture, eventData){
  18938. // create DOM event
  18939. var event = Hammer.DOCUMENT.createEvent('Event');
  18940. event.initEvent(gesture, true, true);
  18941. event.gesture = eventData;
  18942. // trigger on the target if it is in the instance element,
  18943. // this is for event delegation tricks
  18944. var element = this.element;
  18945. if(Hammer.utils.hasParent(eventData.target, element)) {
  18946. element = eventData.target;
  18947. }
  18948. element.dispatchEvent(event);
  18949. return this;
  18950. },
  18951. /**
  18952. * enable of disable hammer.js detection
  18953. * @param {Boolean} state
  18954. * @returns {Hammer.Instance}
  18955. */
  18956. enable: function enable(state) {
  18957. this.enabled = state;
  18958. return this;
  18959. }
  18960. };
  18961. /**
  18962. * this holds the last move event,
  18963. * used to fix empty touchend issue
  18964. * see the onTouch event for an explanation
  18965. * @type {Object}
  18966. */
  18967. var last_move_event = null;
  18968. /**
  18969. * when the mouse is hold down, this is true
  18970. * @type {Boolean}
  18971. */
  18972. var enable_detect = false;
  18973. /**
  18974. * when touch events have been fired, this is true
  18975. * @type {Boolean}
  18976. */
  18977. var touch_triggered = false;
  18978. Hammer.event = {
  18979. /**
  18980. * simple addEventListener
  18981. * @param {HTMLElement} element
  18982. * @param {String} type
  18983. * @param {Function} handler
  18984. */
  18985. bindDom: function(element, type, handler) {
  18986. var types = type.split(' ');
  18987. for(var t=0; t<types.length; t++) {
  18988. element.addEventListener(types[t], handler, false);
  18989. }
  18990. },
  18991. /**
  18992. * touch events with mouse fallback
  18993. * @param {HTMLElement} element
  18994. * @param {String} eventType like Hammer.EVENT_MOVE
  18995. * @param {Function} handler
  18996. */
  18997. onTouch: function onTouch(element, eventType, handler) {
  18998. var self = this;
  18999. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  19000. var sourceEventType = ev.type.toLowerCase();
  19001. // onmouseup, but when touchend has been fired we do nothing.
  19002. // this is for touchdevices which also fire a mouseup on touchend
  19003. if(sourceEventType.match(/mouse/) && touch_triggered) {
  19004. return;
  19005. }
  19006. // mousebutton must be down or a touch event
  19007. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  19008. sourceEventType.match(/pointerdown/) || // pointerevents touch
  19009. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  19010. ){
  19011. enable_detect = true;
  19012. }
  19013. // we are in a touch event, set the touch triggered bool to true,
  19014. // this for the conflicts that may occur on ios and android
  19015. if(sourceEventType.match(/touch|pointer/)) {
  19016. touch_triggered = true;
  19017. }
  19018. // count the total touches on the screen
  19019. var count_touches = 0;
  19020. // when touch has been triggered in this detection session
  19021. // and we are now handling a mouse event, we stop that to prevent conflicts
  19022. if(enable_detect) {
  19023. // update pointerevent
  19024. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  19025. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  19026. }
  19027. // touch
  19028. else if(sourceEventType.match(/touch/)) {
  19029. count_touches = ev.touches.length;
  19030. }
  19031. // mouse
  19032. else if(!touch_triggered) {
  19033. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  19034. }
  19035. // if we are in a end event, but when we remove one touch and
  19036. // we still have enough, set eventType to move
  19037. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  19038. eventType = Hammer.EVENT_MOVE;
  19039. }
  19040. // no touches, force the end event
  19041. else if(!count_touches) {
  19042. eventType = Hammer.EVENT_END;
  19043. }
  19044. // because touchend has no touches, and we often want to use these in our gestures,
  19045. // we send the last move event as our eventData in touchend
  19046. if(!count_touches && last_move_event !== null) {
  19047. ev = last_move_event;
  19048. }
  19049. // store the last move event
  19050. else {
  19051. last_move_event = ev;
  19052. }
  19053. // trigger the handler
  19054. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  19055. // remove pointerevent from list
  19056. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  19057. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  19058. }
  19059. }
  19060. //debug(sourceEventType +" "+ eventType);
  19061. // on the end we reset everything
  19062. if(!count_touches) {
  19063. last_move_event = null;
  19064. enable_detect = false;
  19065. touch_triggered = false;
  19066. Hammer.PointerEvent.reset();
  19067. }
  19068. });
  19069. },
  19070. /**
  19071. * we have different events for each device/browser
  19072. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  19073. */
  19074. determineEventTypes: function determineEventTypes() {
  19075. // determine the eventtype we want to set
  19076. var types;
  19077. // pointerEvents magic
  19078. if(Hammer.HAS_POINTEREVENTS) {
  19079. types = Hammer.PointerEvent.getEvents();
  19080. }
  19081. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  19082. else if(Hammer.NO_MOUSEEVENTS) {
  19083. types = [
  19084. 'touchstart',
  19085. 'touchmove',
  19086. 'touchend touchcancel'];
  19087. }
  19088. // for non pointer events browsers and mixed browsers,
  19089. // like chrome on windows8 touch laptop
  19090. else {
  19091. types = [
  19092. 'touchstart mousedown',
  19093. 'touchmove mousemove',
  19094. 'touchend touchcancel mouseup'];
  19095. }
  19096. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  19097. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  19098. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  19099. },
  19100. /**
  19101. * create touchlist depending on the event
  19102. * @param {Object} ev
  19103. * @param {String} eventType used by the fakemultitouch plugin
  19104. */
  19105. getTouchList: function getTouchList(ev/*, eventType*/) {
  19106. // get the fake pointerEvent touchlist
  19107. if(Hammer.HAS_POINTEREVENTS) {
  19108. return Hammer.PointerEvent.getTouchList();
  19109. }
  19110. // get the touchlist
  19111. else if(ev.touches) {
  19112. return ev.touches;
  19113. }
  19114. // make fake touchlist from mouse position
  19115. else {
  19116. return [{
  19117. identifier: 1,
  19118. pageX: ev.pageX,
  19119. pageY: ev.pageY,
  19120. target: ev.target
  19121. }];
  19122. }
  19123. },
  19124. /**
  19125. * collect event data for Hammer js
  19126. * @param {HTMLElement} element
  19127. * @param {String} eventType like Hammer.EVENT_MOVE
  19128. * @param {Object} eventData
  19129. */
  19130. collectEventData: function collectEventData(element, eventType, ev) {
  19131. var touches = this.getTouchList(ev, eventType);
  19132. // find out pointerType
  19133. var pointerType = Hammer.POINTER_TOUCH;
  19134. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  19135. pointerType = Hammer.POINTER_MOUSE;
  19136. }
  19137. return {
  19138. center : Hammer.utils.getCenter(touches),
  19139. timeStamp : new Date().getTime(),
  19140. target : ev.target,
  19141. touches : touches,
  19142. eventType : eventType,
  19143. pointerType : pointerType,
  19144. srcEvent : ev,
  19145. /**
  19146. * prevent the browser default actions
  19147. * mostly used to disable scrolling of the browser
  19148. */
  19149. preventDefault: function() {
  19150. if(this.srcEvent.preventManipulation) {
  19151. this.srcEvent.preventManipulation();
  19152. }
  19153. if(this.srcEvent.preventDefault) {
  19154. this.srcEvent.preventDefault();
  19155. }
  19156. },
  19157. /**
  19158. * stop bubbling the event up to its parents
  19159. */
  19160. stopPropagation: function() {
  19161. this.srcEvent.stopPropagation();
  19162. },
  19163. /**
  19164. * immediately stop gesture detection
  19165. * might be useful after a swipe was detected
  19166. * @return {*}
  19167. */
  19168. stopDetect: function() {
  19169. return Hammer.detection.stopDetect();
  19170. }
  19171. };
  19172. }
  19173. };
  19174. Hammer.PointerEvent = {
  19175. /**
  19176. * holds all pointers
  19177. * @type {Object}
  19178. */
  19179. pointers: {},
  19180. /**
  19181. * get a list of pointers
  19182. * @returns {Array} touchlist
  19183. */
  19184. getTouchList: function() {
  19185. var self = this;
  19186. var touchlist = [];
  19187. // we can use forEach since pointerEvents only is in IE10
  19188. Object.keys(self.pointers).sort().forEach(function(id) {
  19189. touchlist.push(self.pointers[id]);
  19190. });
  19191. return touchlist;
  19192. },
  19193. /**
  19194. * update the position of a pointer
  19195. * @param {String} type Hammer.EVENT_END
  19196. * @param {Object} pointerEvent
  19197. */
  19198. updatePointer: function(type, pointerEvent) {
  19199. if(type == Hammer.EVENT_END) {
  19200. this.pointers = {};
  19201. }
  19202. else {
  19203. pointerEvent.identifier = pointerEvent.pointerId;
  19204. this.pointers[pointerEvent.pointerId] = pointerEvent;
  19205. }
  19206. return Object.keys(this.pointers).length;
  19207. },
  19208. /**
  19209. * check if ev matches pointertype
  19210. * @param {String} pointerType Hammer.POINTER_MOUSE
  19211. * @param {PointerEvent} ev
  19212. */
  19213. matchType: function(pointerType, ev) {
  19214. if(!ev.pointerType) {
  19215. return false;
  19216. }
  19217. var types = {};
  19218. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  19219. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  19220. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  19221. return types[pointerType];
  19222. },
  19223. /**
  19224. * get events
  19225. */
  19226. getEvents: function() {
  19227. return [
  19228. 'pointerdown MSPointerDown',
  19229. 'pointermove MSPointerMove',
  19230. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  19231. ];
  19232. },
  19233. /**
  19234. * reset the list
  19235. */
  19236. reset: function() {
  19237. this.pointers = {};
  19238. }
  19239. };
  19240. Hammer.utils = {
  19241. /**
  19242. * extend method,
  19243. * also used for cloning when dest is an empty object
  19244. * @param {Object} dest
  19245. * @param {Object} src
  19246. * @parm {Boolean} merge do a merge
  19247. * @returns {Object} dest
  19248. */
  19249. extend: function extend(dest, src, merge) {
  19250. for (var key in src) {
  19251. if(dest[key] !== undefined && merge) {
  19252. continue;
  19253. }
  19254. dest[key] = src[key];
  19255. }
  19256. return dest;
  19257. },
  19258. /**
  19259. * find if a node is in the given parent
  19260. * used for event delegation tricks
  19261. * @param {HTMLElement} node
  19262. * @param {HTMLElement} parent
  19263. * @returns {boolean} has_parent
  19264. */
  19265. hasParent: function(node, parent) {
  19266. while(node){
  19267. if(node == parent) {
  19268. return true;
  19269. }
  19270. node = node.parentNode;
  19271. }
  19272. return false;
  19273. },
  19274. /**
  19275. * get the center of all the touches
  19276. * @param {Array} touches
  19277. * @returns {Object} center
  19278. */
  19279. getCenter: function getCenter(touches) {
  19280. var valuesX = [], valuesY = [];
  19281. for(var t= 0,len=touches.length; t<len; t++) {
  19282. valuesX.push(touches[t].pageX);
  19283. valuesY.push(touches[t].pageY);
  19284. }
  19285. return {
  19286. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  19287. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  19288. };
  19289. },
  19290. /**
  19291. * calculate the velocity between two points
  19292. * @param {Number} delta_time
  19293. * @param {Number} delta_x
  19294. * @param {Number} delta_y
  19295. * @returns {Object} velocity
  19296. */
  19297. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  19298. return {
  19299. x: Math.abs(delta_x / delta_time) || 0,
  19300. y: Math.abs(delta_y / delta_time) || 0
  19301. };
  19302. },
  19303. /**
  19304. * calculate the angle between two coordinates
  19305. * @param {Touch} touch1
  19306. * @param {Touch} touch2
  19307. * @returns {Number} angle
  19308. */
  19309. getAngle: function getAngle(touch1, touch2) {
  19310. var y = touch2.pageY - touch1.pageY,
  19311. x = touch2.pageX - touch1.pageX;
  19312. return Math.atan2(y, x) * 180 / Math.PI;
  19313. },
  19314. /**
  19315. * angle to direction define
  19316. * @param {Touch} touch1
  19317. * @param {Touch} touch2
  19318. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  19319. */
  19320. getDirection: function getDirection(touch1, touch2) {
  19321. var x = Math.abs(touch1.pageX - touch2.pageX),
  19322. y = Math.abs(touch1.pageY - touch2.pageY);
  19323. if(x >= y) {
  19324. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  19325. }
  19326. else {
  19327. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  19328. }
  19329. },
  19330. /**
  19331. * calculate the distance between two touches
  19332. * @param {Touch} touch1
  19333. * @param {Touch} touch2
  19334. * @returns {Number} distance
  19335. */
  19336. getDistance: function getDistance(touch1, touch2) {
  19337. var x = touch2.pageX - touch1.pageX,
  19338. y = touch2.pageY - touch1.pageY;
  19339. return Math.sqrt((x*x) + (y*y));
  19340. },
  19341. /**
  19342. * calculate the scale factor between two touchLists (fingers)
  19343. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  19344. * @param {Array} start
  19345. * @param {Array} end
  19346. * @returns {Number} scale
  19347. */
  19348. getScale: function getScale(start, end) {
  19349. // need two fingers...
  19350. if(start.length >= 2 && end.length >= 2) {
  19351. return this.getDistance(end[0], end[1]) /
  19352. this.getDistance(start[0], start[1]);
  19353. }
  19354. return 1;
  19355. },
  19356. /**
  19357. * calculate the rotation degrees between two touchLists (fingers)
  19358. * @param {Array} start
  19359. * @param {Array} end
  19360. * @returns {Number} rotation
  19361. */
  19362. getRotation: function getRotation(start, end) {
  19363. // need two fingers
  19364. if(start.length >= 2 && end.length >= 2) {
  19365. return this.getAngle(end[1], end[0]) -
  19366. this.getAngle(start[1], start[0]);
  19367. }
  19368. return 0;
  19369. },
  19370. /**
  19371. * boolean if the direction is vertical
  19372. * @param {String} direction
  19373. * @returns {Boolean} is_vertical
  19374. */
  19375. isVertical: function isVertical(direction) {
  19376. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  19377. },
  19378. /**
  19379. * stop browser default behavior with css props
  19380. * @param {HtmlElement} element
  19381. * @param {Object} css_props
  19382. */
  19383. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  19384. var prop,
  19385. vendors = ['webkit','khtml','moz','ms','o',''];
  19386. if(!css_props || !element.style) {
  19387. return;
  19388. }
  19389. // with css properties for modern browsers
  19390. for(var i = 0; i < vendors.length; i++) {
  19391. for(var p in css_props) {
  19392. if(css_props.hasOwnProperty(p)) {
  19393. prop = p;
  19394. // vender prefix at the property
  19395. if(vendors[i]) {
  19396. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  19397. }
  19398. // set the style
  19399. element.style[prop] = css_props[p];
  19400. }
  19401. }
  19402. }
  19403. // also the disable onselectstart
  19404. if(css_props.userSelect == 'none') {
  19405. element.onselectstart = function() {
  19406. return false;
  19407. };
  19408. }
  19409. }
  19410. };
  19411. Hammer.detection = {
  19412. // contains all registred Hammer.gestures in the correct order
  19413. gestures: [],
  19414. // data of the current Hammer.gesture detection session
  19415. current: null,
  19416. // the previous Hammer.gesture session data
  19417. // is a full clone of the previous gesture.current object
  19418. previous: null,
  19419. // when this becomes true, no gestures are fired
  19420. stopped: false,
  19421. /**
  19422. * start Hammer.gesture detection
  19423. * @param {Hammer.Instance} inst
  19424. * @param {Object} eventData
  19425. */
  19426. startDetect: function startDetect(inst, eventData) {
  19427. // already busy with a Hammer.gesture detection on an element
  19428. if(this.current) {
  19429. return;
  19430. }
  19431. this.stopped = false;
  19432. this.current = {
  19433. inst : inst, // reference to HammerInstance we're working for
  19434. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  19435. lastEvent : false, // last eventData
  19436. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  19437. };
  19438. this.detect(eventData);
  19439. },
  19440. /**
  19441. * Hammer.gesture detection
  19442. * @param {Object} eventData
  19443. * @param {Object} eventData
  19444. */
  19445. detect: function detect(eventData) {
  19446. if(!this.current || this.stopped) {
  19447. return;
  19448. }
  19449. // extend event data with calculations about scale, distance etc
  19450. eventData = this.extendEventData(eventData);
  19451. // instance options
  19452. var inst_options = this.current.inst.options;
  19453. // call Hammer.gesture handlers
  19454. for(var g=0,len=this.gestures.length; g<len; g++) {
  19455. var gesture = this.gestures[g];
  19456. // only when the instance options have enabled this gesture
  19457. if(!this.stopped && inst_options[gesture.name] !== false) {
  19458. // if a handler returns false, we stop with the detection
  19459. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  19460. this.stopDetect();
  19461. break;
  19462. }
  19463. }
  19464. }
  19465. // store as previous event event
  19466. if(this.current) {
  19467. this.current.lastEvent = eventData;
  19468. }
  19469. // endevent, but not the last touch, so dont stop
  19470. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  19471. this.stopDetect();
  19472. }
  19473. return eventData;
  19474. },
  19475. /**
  19476. * clear the Hammer.gesture vars
  19477. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  19478. * to stop other Hammer.gestures from being fired
  19479. */
  19480. stopDetect: function stopDetect() {
  19481. // clone current data to the store as the previous gesture
  19482. // used for the double tap gesture, since this is an other gesture detect session
  19483. this.previous = Hammer.utils.extend({}, this.current);
  19484. // reset the current
  19485. this.current = null;
  19486. // stopped!
  19487. this.stopped = true;
  19488. },
  19489. /**
  19490. * extend eventData for Hammer.gestures
  19491. * @param {Object} ev
  19492. * @returns {Object} ev
  19493. */
  19494. extendEventData: function extendEventData(ev) {
  19495. var startEv = this.current.startEvent;
  19496. // if the touches change, set the new touches over the startEvent touches
  19497. // this because touchevents don't have all the touches on touchstart, or the
  19498. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  19499. // but, sometimes it happens that both fingers are touching at the EXACT same time
  19500. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  19501. // extend 1 level deep to get the touchlist with the touch objects
  19502. startEv.touches = [];
  19503. for(var i=0,len=ev.touches.length; i<len; i++) {
  19504. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  19505. }
  19506. }
  19507. var delta_time = ev.timeStamp - startEv.timeStamp,
  19508. delta_x = ev.center.pageX - startEv.center.pageX,
  19509. delta_y = ev.center.pageY - startEv.center.pageY,
  19510. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  19511. Hammer.utils.extend(ev, {
  19512. deltaTime : delta_time,
  19513. deltaX : delta_x,
  19514. deltaY : delta_y,
  19515. velocityX : velocity.x,
  19516. velocityY : velocity.y,
  19517. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  19518. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  19519. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  19520. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  19521. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  19522. startEvent : startEv
  19523. });
  19524. return ev;
  19525. },
  19526. /**
  19527. * register new gesture
  19528. * @param {Object} gesture object, see gestures.js for documentation
  19529. * @returns {Array} gestures
  19530. */
  19531. register: function register(gesture) {
  19532. // add an enable gesture options if there is no given
  19533. var options = gesture.defaults || {};
  19534. if(options[gesture.name] === undefined) {
  19535. options[gesture.name] = true;
  19536. }
  19537. // extend Hammer default options with the Hammer.gesture options
  19538. Hammer.utils.extend(Hammer.defaults, options, true);
  19539. // set its index
  19540. gesture.index = gesture.index || 1000;
  19541. // add Hammer.gesture to the list
  19542. this.gestures.push(gesture);
  19543. // sort the list by index
  19544. this.gestures.sort(function(a, b) {
  19545. if (a.index < b.index) {
  19546. return -1;
  19547. }
  19548. if (a.index > b.index) {
  19549. return 1;
  19550. }
  19551. return 0;
  19552. });
  19553. return this.gestures;
  19554. }
  19555. };
  19556. Hammer.gestures = Hammer.gestures || {};
  19557. /**
  19558. * Custom gestures
  19559. * ==============================
  19560. *
  19561. * Gesture object
  19562. * --------------------
  19563. * The object structure of a gesture:
  19564. *
  19565. * { name: 'mygesture',
  19566. * index: 1337,
  19567. * defaults: {
  19568. * mygesture_option: true
  19569. * }
  19570. * handler: function(type, ev, inst) {
  19571. * // trigger gesture event
  19572. * inst.trigger(this.name, ev);
  19573. * }
  19574. * }
  19575. * @param {String} name
  19576. * this should be the name of the gesture, lowercase
  19577. * it is also being used to disable/enable the gesture per instance config.
  19578. *
  19579. * @param {Number} [index=1000]
  19580. * the index of the gesture, where it is going to be in the stack of gestures detection
  19581. * like when you build an gesture that depends on the drag gesture, it is a good
  19582. * idea to place it after the index of the drag gesture.
  19583. *
  19584. * @param {Object} [defaults={}]
  19585. * the default settings of the gesture. these are added to the instance settings,
  19586. * and can be overruled per instance. you can also add the name of the gesture,
  19587. * but this is also added by default (and set to true).
  19588. *
  19589. * @param {Function} handler
  19590. * this handles the gesture detection of your custom gesture and receives the
  19591. * following arguments:
  19592. *
  19593. * @param {Object} eventData
  19594. * event data containing the following properties:
  19595. * timeStamp {Number} time the event occurred
  19596. * target {HTMLElement} target element
  19597. * touches {Array} touches (fingers, pointers, mouse) on the screen
  19598. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  19599. * center {Object} center position of the touches. contains pageX and pageY
  19600. * deltaTime {Number} the total time of the touches in the screen
  19601. * deltaX {Number} the delta on x axis we haved moved
  19602. * deltaY {Number} the delta on y axis we haved moved
  19603. * velocityX {Number} the velocity on the x
  19604. * velocityY {Number} the velocity on y
  19605. * angle {Number} the angle we are moving
  19606. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  19607. * distance {Number} the distance we haved moved
  19608. * scale {Number} scaling of the touches, needs 2 touches
  19609. * rotation {Number} rotation of the touches, needs 2 touches *
  19610. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  19611. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  19612. * startEvent {Object} contains the same properties as above,
  19613. * but from the first touch. this is used to calculate
  19614. * distances, deltaTime, scaling etc
  19615. *
  19616. * @param {Hammer.Instance} inst
  19617. * the instance we are doing the detection for. you can get the options from
  19618. * the inst.options object and trigger the gesture event by calling inst.trigger
  19619. *
  19620. *
  19621. * Handle gestures
  19622. * --------------------
  19623. * inside the handler you can get/set Hammer.detection.current. This is the current
  19624. * detection session. It has the following properties
  19625. * @param {String} name
  19626. * contains the name of the gesture we have detected. it has not a real function,
  19627. * only to check in other gestures if something is detected.
  19628. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  19629. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  19630. *
  19631. * @readonly
  19632. * @param {Hammer.Instance} inst
  19633. * the instance we do the detection for
  19634. *
  19635. * @readonly
  19636. * @param {Object} startEvent
  19637. * contains the properties of the first gesture detection in this session.
  19638. * Used for calculations about timing, distance, etc.
  19639. *
  19640. * @readonly
  19641. * @param {Object} lastEvent
  19642. * contains all the properties of the last gesture detect in this session.
  19643. *
  19644. * after the gesture detection session has been completed (user has released the screen)
  19645. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  19646. * this is usefull for gestures like doubletap, where you need to know if the
  19647. * previous gesture was a tap
  19648. *
  19649. * options that have been set by the instance can be received by calling inst.options
  19650. *
  19651. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  19652. * The first param is the name of your gesture, the second the event argument
  19653. *
  19654. *
  19655. * Register gestures
  19656. * --------------------
  19657. * When an gesture is added to the Hammer.gestures object, it is auto registered
  19658. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  19659. * manually and pass your gesture object as a param
  19660. *
  19661. */
  19662. /**
  19663. * Hold
  19664. * Touch stays at the same place for x time
  19665. * @events hold
  19666. */
  19667. Hammer.gestures.Hold = {
  19668. name: 'hold',
  19669. index: 10,
  19670. defaults: {
  19671. hold_timeout : 500,
  19672. hold_threshold : 1
  19673. },
  19674. timer: null,
  19675. handler: function holdGesture(ev, inst) {
  19676. switch(ev.eventType) {
  19677. case Hammer.EVENT_START:
  19678. // clear any running timers
  19679. clearTimeout(this.timer);
  19680. // set the gesture so we can check in the timeout if it still is
  19681. Hammer.detection.current.name = this.name;
  19682. // set timer and if after the timeout it still is hold,
  19683. // we trigger the hold event
  19684. this.timer = setTimeout(function() {
  19685. if(Hammer.detection.current.name == 'hold') {
  19686. inst.trigger('hold', ev);
  19687. }
  19688. }, inst.options.hold_timeout);
  19689. break;
  19690. // when you move or end we clear the timer
  19691. case Hammer.EVENT_MOVE:
  19692. if(ev.distance > inst.options.hold_threshold) {
  19693. clearTimeout(this.timer);
  19694. }
  19695. break;
  19696. case Hammer.EVENT_END:
  19697. clearTimeout(this.timer);
  19698. break;
  19699. }
  19700. }
  19701. };
  19702. /**
  19703. * Tap/DoubleTap
  19704. * Quick touch at a place or double at the same place
  19705. * @events tap, doubletap
  19706. */
  19707. Hammer.gestures.Tap = {
  19708. name: 'tap',
  19709. index: 100,
  19710. defaults: {
  19711. tap_max_touchtime : 250,
  19712. tap_max_distance : 10,
  19713. tap_always : true,
  19714. doubletap_distance : 20,
  19715. doubletap_interval : 300
  19716. },
  19717. handler: function tapGesture(ev, inst) {
  19718. if(ev.eventType == Hammer.EVENT_END) {
  19719. // previous gesture, for the double tap since these are two different gesture detections
  19720. var prev = Hammer.detection.previous,
  19721. did_doubletap = false;
  19722. // when the touchtime is higher then the max touch time
  19723. // or when the moving distance is too much
  19724. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  19725. ev.distance > inst.options.tap_max_distance) {
  19726. return;
  19727. }
  19728. // check if double tap
  19729. if(prev && prev.name == 'tap' &&
  19730. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  19731. ev.distance < inst.options.doubletap_distance) {
  19732. inst.trigger('doubletap', ev);
  19733. did_doubletap = true;
  19734. }
  19735. // do a single tap
  19736. if(!did_doubletap || inst.options.tap_always) {
  19737. Hammer.detection.current.name = 'tap';
  19738. inst.trigger(Hammer.detection.current.name, ev);
  19739. }
  19740. }
  19741. }
  19742. };
  19743. /**
  19744. * Swipe
  19745. * triggers swipe events when the end velocity is above the threshold
  19746. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  19747. */
  19748. Hammer.gestures.Swipe = {
  19749. name: 'swipe',
  19750. index: 40,
  19751. defaults: {
  19752. // set 0 for unlimited, but this can conflict with transform
  19753. swipe_max_touches : 1,
  19754. swipe_velocity : 0.7
  19755. },
  19756. handler: function swipeGesture(ev, inst) {
  19757. if(ev.eventType == Hammer.EVENT_END) {
  19758. // max touches
  19759. if(inst.options.swipe_max_touches > 0 &&
  19760. ev.touches.length > inst.options.swipe_max_touches) {
  19761. return;
  19762. }
  19763. // when the distance we moved is too small we skip this gesture
  19764. // or we can be already in dragging
  19765. if(ev.velocityX > inst.options.swipe_velocity ||
  19766. ev.velocityY > inst.options.swipe_velocity) {
  19767. // trigger swipe events
  19768. inst.trigger(this.name, ev);
  19769. inst.trigger(this.name + ev.direction, ev);
  19770. }
  19771. }
  19772. }
  19773. };
  19774. /**
  19775. * Drag
  19776. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  19777. * moving left and right is a good practice. When all the drag events are blocking
  19778. * you disable scrolling on that area.
  19779. * @events drag, drapleft, dragright, dragup, dragdown
  19780. */
  19781. Hammer.gestures.Drag = {
  19782. name: 'drag',
  19783. index: 50,
  19784. defaults: {
  19785. drag_min_distance : 10,
  19786. // set 0 for unlimited, but this can conflict with transform
  19787. drag_max_touches : 1,
  19788. // prevent default browser behavior when dragging occurs
  19789. // be careful with it, it makes the element a blocking element
  19790. // when you are using the drag gesture, it is a good practice to set this true
  19791. drag_block_horizontal : false,
  19792. drag_block_vertical : false,
  19793. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  19794. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  19795. drag_lock_to_axis : false,
  19796. // drag lock only kicks in when distance > drag_lock_min_distance
  19797. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  19798. drag_lock_min_distance : 25
  19799. },
  19800. triggered: false,
  19801. handler: function dragGesture(ev, inst) {
  19802. // current gesture isnt drag, but dragged is true
  19803. // this means an other gesture is busy. now call dragend
  19804. if(Hammer.detection.current.name != this.name && this.triggered) {
  19805. inst.trigger(this.name +'end', ev);
  19806. this.triggered = false;
  19807. return;
  19808. }
  19809. // max touches
  19810. if(inst.options.drag_max_touches > 0 &&
  19811. ev.touches.length > inst.options.drag_max_touches) {
  19812. return;
  19813. }
  19814. switch(ev.eventType) {
  19815. case Hammer.EVENT_START:
  19816. this.triggered = false;
  19817. break;
  19818. case Hammer.EVENT_MOVE:
  19819. // when the distance we moved is too small we skip this gesture
  19820. // or we can be already in dragging
  19821. if(ev.distance < inst.options.drag_min_distance &&
  19822. Hammer.detection.current.name != this.name) {
  19823. return;
  19824. }
  19825. // we are dragging!
  19826. Hammer.detection.current.name = this.name;
  19827. // lock drag to axis?
  19828. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  19829. ev.drag_locked_to_axis = true;
  19830. }
  19831. var last_direction = Hammer.detection.current.lastEvent.direction;
  19832. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  19833. // keep direction on the axis that the drag gesture started on
  19834. if(Hammer.utils.isVertical(last_direction)) {
  19835. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  19836. }
  19837. else {
  19838. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  19839. }
  19840. }
  19841. // first time, trigger dragstart event
  19842. if(!this.triggered) {
  19843. inst.trigger(this.name +'start', ev);
  19844. this.triggered = true;
  19845. }
  19846. // trigger normal event
  19847. inst.trigger(this.name, ev);
  19848. // direction event, like dragdown
  19849. inst.trigger(this.name + ev.direction, ev);
  19850. // block the browser events
  19851. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  19852. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  19853. ev.preventDefault();
  19854. }
  19855. break;
  19856. case Hammer.EVENT_END:
  19857. // trigger dragend
  19858. if(this.triggered) {
  19859. inst.trigger(this.name +'end', ev);
  19860. }
  19861. this.triggered = false;
  19862. break;
  19863. }
  19864. }
  19865. };
  19866. /**
  19867. * Transform
  19868. * User want to scale or rotate with 2 fingers
  19869. * @events transform, pinch, pinchin, pinchout, rotate
  19870. */
  19871. Hammer.gestures.Transform = {
  19872. name: 'transform',
  19873. index: 45,
  19874. defaults: {
  19875. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  19876. transform_min_scale : 0.01,
  19877. // rotation in degrees
  19878. transform_min_rotation : 1,
  19879. // prevent default browser behavior when two touches are on the screen
  19880. // but it makes the element a blocking element
  19881. // when you are using the transform gesture, it is a good practice to set this true
  19882. transform_always_block : false
  19883. },
  19884. triggered: false,
  19885. handler: function transformGesture(ev, inst) {
  19886. // current gesture isnt drag, but dragged is true
  19887. // this means an other gesture is busy. now call dragend
  19888. if(Hammer.detection.current.name != this.name && this.triggered) {
  19889. inst.trigger(this.name +'end', ev);
  19890. this.triggered = false;
  19891. return;
  19892. }
  19893. // atleast multitouch
  19894. if(ev.touches.length < 2) {
  19895. return;
  19896. }
  19897. // prevent default when two fingers are on the screen
  19898. if(inst.options.transform_always_block) {
  19899. ev.preventDefault();
  19900. }
  19901. switch(ev.eventType) {
  19902. case Hammer.EVENT_START:
  19903. this.triggered = false;
  19904. break;
  19905. case Hammer.EVENT_MOVE:
  19906. var scale_threshold = Math.abs(1-ev.scale);
  19907. var rotation_threshold = Math.abs(ev.rotation);
  19908. // when the distance we moved is too small we skip this gesture
  19909. // or we can be already in dragging
  19910. if(scale_threshold < inst.options.transform_min_scale &&
  19911. rotation_threshold < inst.options.transform_min_rotation) {
  19912. return;
  19913. }
  19914. // we are transforming!
  19915. Hammer.detection.current.name = this.name;
  19916. // first time, trigger dragstart event
  19917. if(!this.triggered) {
  19918. inst.trigger(this.name +'start', ev);
  19919. this.triggered = true;
  19920. }
  19921. inst.trigger(this.name, ev); // basic transform event
  19922. // trigger rotate event
  19923. if(rotation_threshold > inst.options.transform_min_rotation) {
  19924. inst.trigger('rotate', ev);
  19925. }
  19926. // trigger pinch event
  19927. if(scale_threshold > inst.options.transform_min_scale) {
  19928. inst.trigger('pinch', ev);
  19929. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  19930. }
  19931. break;
  19932. case Hammer.EVENT_END:
  19933. // trigger dragend
  19934. if(this.triggered) {
  19935. inst.trigger(this.name +'end', ev);
  19936. }
  19937. this.triggered = false;
  19938. break;
  19939. }
  19940. }
  19941. };
  19942. /**
  19943. * Touch
  19944. * Called as first, tells the user has touched the screen
  19945. * @events touch
  19946. */
  19947. Hammer.gestures.Touch = {
  19948. name: 'touch',
  19949. index: -Infinity,
  19950. defaults: {
  19951. // call preventDefault at touchstart, and makes the element blocking by
  19952. // disabling the scrolling of the page, but it improves gestures like
  19953. // transforming and dragging.
  19954. // be careful with using this, it can be very annoying for users to be stuck
  19955. // on the page
  19956. prevent_default: false,
  19957. // disable mouse events, so only touch (or pen!) input triggers events
  19958. prevent_mouseevents: false
  19959. },
  19960. handler: function touchGesture(ev, inst) {
  19961. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  19962. ev.stopDetect();
  19963. return;
  19964. }
  19965. if(inst.options.prevent_default) {
  19966. ev.preventDefault();
  19967. }
  19968. if(ev.eventType == Hammer.EVENT_START) {
  19969. inst.trigger(this.name, ev);
  19970. }
  19971. }
  19972. };
  19973. /**
  19974. * Release
  19975. * Called as last, tells the user has released the screen
  19976. * @events release
  19977. */
  19978. Hammer.gestures.Release = {
  19979. name: 'release',
  19980. index: Infinity,
  19981. handler: function releaseGesture(ev, inst) {
  19982. if(ev.eventType == Hammer.EVENT_END) {
  19983. inst.trigger(this.name, ev);
  19984. }
  19985. }
  19986. };
  19987. // node export
  19988. if(typeof module === 'object' && typeof module.exports === 'object'){
  19989. module.exports = Hammer;
  19990. }
  19991. // just window export
  19992. else {
  19993. window.Hammer = Hammer;
  19994. // requireJS module definition
  19995. if(typeof window.define === 'function' && window.define.amd) {
  19996. window.define('hammer', [], function() {
  19997. return Hammer;
  19998. });
  19999. }
  20000. }
  20001. })(this);
  20002. },{}],4:[function(require,module,exports){
  20003. var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};//! moment.js
  20004. //! version : 2.6.0
  20005. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  20006. //! license : MIT
  20007. //! momentjs.com
  20008. (function (undefined) {
  20009. /************************************
  20010. Constants
  20011. ************************************/
  20012. var moment,
  20013. VERSION = "2.6.0",
  20014. // the global-scope this is NOT the global object in Node.js
  20015. globalScope = typeof global !== 'undefined' ? global : this,
  20016. oldGlobalMoment,
  20017. round = Math.round,
  20018. i,
  20019. YEAR = 0,
  20020. MONTH = 1,
  20021. DATE = 2,
  20022. HOUR = 3,
  20023. MINUTE = 4,
  20024. SECOND = 5,
  20025. MILLISECOND = 6,
  20026. // internal storage for language config files
  20027. languages = {},
  20028. // moment internal properties
  20029. momentProperties = {
  20030. _isAMomentObject: null,
  20031. _i : null,
  20032. _f : null,
  20033. _l : null,
  20034. _strict : null,
  20035. _isUTC : null,
  20036. _offset : null, // optional. Combine with _isUTC
  20037. _pf : null,
  20038. _lang : null // optional
  20039. },
  20040. // check for nodeJS
  20041. hasModule = (typeof module !== 'undefined' && module.exports),
  20042. // ASP.NET json date format regex
  20043. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  20044. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  20045. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  20046. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  20047. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  20048. // format tokens
  20049. 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,
  20050. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  20051. // parsing token regexes
  20052. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  20053. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  20054. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  20055. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  20056. parseTokenDigits = /\d+/, // nonzero number of digits
  20057. 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.
  20058. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  20059. parseTokenT = /T/i, // T (ISO separator)
  20060. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  20061. parseTokenOrdinal = /\d{1,2}/,
  20062. //strict parsing regexes
  20063. parseTokenOneDigit = /\d/, // 0 - 9
  20064. parseTokenTwoDigits = /\d\d/, // 00 - 99
  20065. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  20066. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  20067. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  20068. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  20069. // iso 8601 regex
  20070. // 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)
  20071. 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)?)?$/,
  20072. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  20073. isoDates = [
  20074. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  20075. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  20076. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  20077. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  20078. ['YYYY-DDD', /\d{4}-\d{3}/]
  20079. ],
  20080. // iso time formats and regexes
  20081. isoTimes = [
  20082. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/],
  20083. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  20084. ['HH:mm', /(T| )\d\d:\d\d/],
  20085. ['HH', /(T| )\d\d/]
  20086. ],
  20087. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  20088. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  20089. // getter and setter names
  20090. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  20091. unitMillisecondFactors = {
  20092. 'Milliseconds' : 1,
  20093. 'Seconds' : 1e3,
  20094. 'Minutes' : 6e4,
  20095. 'Hours' : 36e5,
  20096. 'Days' : 864e5,
  20097. 'Months' : 2592e6,
  20098. 'Years' : 31536e6
  20099. },
  20100. unitAliases = {
  20101. ms : 'millisecond',
  20102. s : 'second',
  20103. m : 'minute',
  20104. h : 'hour',
  20105. d : 'day',
  20106. D : 'date',
  20107. w : 'week',
  20108. W : 'isoWeek',
  20109. M : 'month',
  20110. Q : 'quarter',
  20111. y : 'year',
  20112. DDD : 'dayOfYear',
  20113. e : 'weekday',
  20114. E : 'isoWeekday',
  20115. gg: 'weekYear',
  20116. GG: 'isoWeekYear'
  20117. },
  20118. camelFunctions = {
  20119. dayofyear : 'dayOfYear',
  20120. isoweekday : 'isoWeekday',
  20121. isoweek : 'isoWeek',
  20122. weekyear : 'weekYear',
  20123. isoweekyear : 'isoWeekYear'
  20124. },
  20125. // format function strings
  20126. formatFunctions = {},
  20127. // tokens to ordinalize and pad
  20128. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  20129. paddedTokens = 'M D H h m s w W'.split(' '),
  20130. formatTokenFunctions = {
  20131. M : function () {
  20132. return this.month() + 1;
  20133. },
  20134. MMM : function (format) {
  20135. return this.lang().monthsShort(this, format);
  20136. },
  20137. MMMM : function (format) {
  20138. return this.lang().months(this, format);
  20139. },
  20140. D : function () {
  20141. return this.date();
  20142. },
  20143. DDD : function () {
  20144. return this.dayOfYear();
  20145. },
  20146. d : function () {
  20147. return this.day();
  20148. },
  20149. dd : function (format) {
  20150. return this.lang().weekdaysMin(this, format);
  20151. },
  20152. ddd : function (format) {
  20153. return this.lang().weekdaysShort(this, format);
  20154. },
  20155. dddd : function (format) {
  20156. return this.lang().weekdays(this, format);
  20157. },
  20158. w : function () {
  20159. return this.week();
  20160. },
  20161. W : function () {
  20162. return this.isoWeek();
  20163. },
  20164. YY : function () {
  20165. return leftZeroFill(this.year() % 100, 2);
  20166. },
  20167. YYYY : function () {
  20168. return leftZeroFill(this.year(), 4);
  20169. },
  20170. YYYYY : function () {
  20171. return leftZeroFill(this.year(), 5);
  20172. },
  20173. YYYYYY : function () {
  20174. var y = this.year(), sign = y >= 0 ? '+' : '-';
  20175. return sign + leftZeroFill(Math.abs(y), 6);
  20176. },
  20177. gg : function () {
  20178. return leftZeroFill(this.weekYear() % 100, 2);
  20179. },
  20180. gggg : function () {
  20181. return leftZeroFill(this.weekYear(), 4);
  20182. },
  20183. ggggg : function () {
  20184. return leftZeroFill(this.weekYear(), 5);
  20185. },
  20186. GG : function () {
  20187. return leftZeroFill(this.isoWeekYear() % 100, 2);
  20188. },
  20189. GGGG : function () {
  20190. return leftZeroFill(this.isoWeekYear(), 4);
  20191. },
  20192. GGGGG : function () {
  20193. return leftZeroFill(this.isoWeekYear(), 5);
  20194. },
  20195. e : function () {
  20196. return this.weekday();
  20197. },
  20198. E : function () {
  20199. return this.isoWeekday();
  20200. },
  20201. a : function () {
  20202. return this.lang().meridiem(this.hours(), this.minutes(), true);
  20203. },
  20204. A : function () {
  20205. return this.lang().meridiem(this.hours(), this.minutes(), false);
  20206. },
  20207. H : function () {
  20208. return this.hours();
  20209. },
  20210. h : function () {
  20211. return this.hours() % 12 || 12;
  20212. },
  20213. m : function () {
  20214. return this.minutes();
  20215. },
  20216. s : function () {
  20217. return this.seconds();
  20218. },
  20219. S : function () {
  20220. return toInt(this.milliseconds() / 100);
  20221. },
  20222. SS : function () {
  20223. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  20224. },
  20225. SSS : function () {
  20226. return leftZeroFill(this.milliseconds(), 3);
  20227. },
  20228. SSSS : function () {
  20229. return leftZeroFill(this.milliseconds(), 3);
  20230. },
  20231. Z : function () {
  20232. var a = -this.zone(),
  20233. b = "+";
  20234. if (a < 0) {
  20235. a = -a;
  20236. b = "-";
  20237. }
  20238. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  20239. },
  20240. ZZ : function () {
  20241. var a = -this.zone(),
  20242. b = "+";
  20243. if (a < 0) {
  20244. a = -a;
  20245. b = "-";
  20246. }
  20247. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  20248. },
  20249. z : function () {
  20250. return this.zoneAbbr();
  20251. },
  20252. zz : function () {
  20253. return this.zoneName();
  20254. },
  20255. X : function () {
  20256. return this.unix();
  20257. },
  20258. Q : function () {
  20259. return this.quarter();
  20260. }
  20261. },
  20262. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  20263. function defaultParsingFlags() {
  20264. // We need to deep clone this object, and es5 standard is not very
  20265. // helpful.
  20266. return {
  20267. empty : false,
  20268. unusedTokens : [],
  20269. unusedInput : [],
  20270. overflow : -2,
  20271. charsLeftOver : 0,
  20272. nullInput : false,
  20273. invalidMonth : null,
  20274. invalidFormat : false,
  20275. userInvalidated : false,
  20276. iso: false
  20277. };
  20278. }
  20279. function deprecate(msg, fn) {
  20280. var firstTime = true;
  20281. function printMsg() {
  20282. if (moment.suppressDeprecationWarnings === false &&
  20283. typeof console !== 'undefined' && console.warn) {
  20284. console.warn("Deprecation warning: " + msg);
  20285. }
  20286. }
  20287. return extend(function () {
  20288. if (firstTime) {
  20289. printMsg();
  20290. firstTime = false;
  20291. }
  20292. return fn.apply(this, arguments);
  20293. }, fn);
  20294. }
  20295. function padToken(func, count) {
  20296. return function (a) {
  20297. return leftZeroFill(func.call(this, a), count);
  20298. };
  20299. }
  20300. function ordinalizeToken(func, period) {
  20301. return function (a) {
  20302. return this.lang().ordinal(func.call(this, a), period);
  20303. };
  20304. }
  20305. while (ordinalizeTokens.length) {
  20306. i = ordinalizeTokens.pop();
  20307. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  20308. }
  20309. while (paddedTokens.length) {
  20310. i = paddedTokens.pop();
  20311. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  20312. }
  20313. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  20314. /************************************
  20315. Constructors
  20316. ************************************/
  20317. function Language() {
  20318. }
  20319. // Moment prototype object
  20320. function Moment(config) {
  20321. checkOverflow(config);
  20322. extend(this, config);
  20323. }
  20324. // Duration Constructor
  20325. function Duration(duration) {
  20326. var normalizedInput = normalizeObjectUnits(duration),
  20327. years = normalizedInput.year || 0,
  20328. quarters = normalizedInput.quarter || 0,
  20329. months = normalizedInput.month || 0,
  20330. weeks = normalizedInput.week || 0,
  20331. days = normalizedInput.day || 0,
  20332. hours = normalizedInput.hour || 0,
  20333. minutes = normalizedInput.minute || 0,
  20334. seconds = normalizedInput.second || 0,
  20335. milliseconds = normalizedInput.millisecond || 0;
  20336. // representation for dateAddRemove
  20337. this._milliseconds = +milliseconds +
  20338. seconds * 1e3 + // 1000
  20339. minutes * 6e4 + // 1000 * 60
  20340. hours * 36e5; // 1000 * 60 * 60
  20341. // Because of dateAddRemove treats 24 hours as different from a
  20342. // day when working around DST, we need to store them separately
  20343. this._days = +days +
  20344. weeks * 7;
  20345. // It is impossible translate months into days without knowing
  20346. // which months you are are talking about, so we have to store
  20347. // it separately.
  20348. this._months = +months +
  20349. quarters * 3 +
  20350. years * 12;
  20351. this._data = {};
  20352. this._bubble();
  20353. }
  20354. /************************************
  20355. Helpers
  20356. ************************************/
  20357. function extend(a, b) {
  20358. for (var i in b) {
  20359. if (b.hasOwnProperty(i)) {
  20360. a[i] = b[i];
  20361. }
  20362. }
  20363. if (b.hasOwnProperty("toString")) {
  20364. a.toString = b.toString;
  20365. }
  20366. if (b.hasOwnProperty("valueOf")) {
  20367. a.valueOf = b.valueOf;
  20368. }
  20369. return a;
  20370. }
  20371. function cloneMoment(m) {
  20372. var result = {}, i;
  20373. for (i in m) {
  20374. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  20375. result[i] = m[i];
  20376. }
  20377. }
  20378. return result;
  20379. }
  20380. function absRound(number) {
  20381. if (number < 0) {
  20382. return Math.ceil(number);
  20383. } else {
  20384. return Math.floor(number);
  20385. }
  20386. }
  20387. // left zero fill a number
  20388. // see http://jsperf.com/left-zero-filling for performance comparison
  20389. function leftZeroFill(number, targetLength, forceSign) {
  20390. var output = '' + Math.abs(number),
  20391. sign = number >= 0;
  20392. while (output.length < targetLength) {
  20393. output = '0' + output;
  20394. }
  20395. return (sign ? (forceSign ? '+' : '') : '-') + output;
  20396. }
  20397. // helper function for _.addTime and _.subtractTime
  20398. function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) {
  20399. var milliseconds = duration._milliseconds,
  20400. days = duration._days,
  20401. months = duration._months;
  20402. updateOffset = updateOffset == null ? true : updateOffset;
  20403. if (milliseconds) {
  20404. mom._d.setTime(+mom._d + milliseconds * isAdding);
  20405. }
  20406. if (days) {
  20407. rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding);
  20408. }
  20409. if (months) {
  20410. rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding);
  20411. }
  20412. if (updateOffset) {
  20413. moment.updateOffset(mom, days || months);
  20414. }
  20415. }
  20416. // check if is an array
  20417. function isArray(input) {
  20418. return Object.prototype.toString.call(input) === '[object Array]';
  20419. }
  20420. function isDate(input) {
  20421. return Object.prototype.toString.call(input) === '[object Date]' ||
  20422. input instanceof Date;
  20423. }
  20424. // compare two arrays, return the number of differences
  20425. function compareArrays(array1, array2, dontConvert) {
  20426. var len = Math.min(array1.length, array2.length),
  20427. lengthDiff = Math.abs(array1.length - array2.length),
  20428. diffs = 0,
  20429. i;
  20430. for (i = 0; i < len; i++) {
  20431. if ((dontConvert && array1[i] !== array2[i]) ||
  20432. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  20433. diffs++;
  20434. }
  20435. }
  20436. return diffs + lengthDiff;
  20437. }
  20438. function normalizeUnits(units) {
  20439. if (units) {
  20440. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  20441. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  20442. }
  20443. return units;
  20444. }
  20445. function normalizeObjectUnits(inputObject) {
  20446. var normalizedInput = {},
  20447. normalizedProp,
  20448. prop;
  20449. for (prop in inputObject) {
  20450. if (inputObject.hasOwnProperty(prop)) {
  20451. normalizedProp = normalizeUnits(prop);
  20452. if (normalizedProp) {
  20453. normalizedInput[normalizedProp] = inputObject[prop];
  20454. }
  20455. }
  20456. }
  20457. return normalizedInput;
  20458. }
  20459. function makeList(field) {
  20460. var count, setter;
  20461. if (field.indexOf('week') === 0) {
  20462. count = 7;
  20463. setter = 'day';
  20464. }
  20465. else if (field.indexOf('month') === 0) {
  20466. count = 12;
  20467. setter = 'month';
  20468. }
  20469. else {
  20470. return;
  20471. }
  20472. moment[field] = function (format, index) {
  20473. var i, getter,
  20474. method = moment.fn._lang[field],
  20475. results = [];
  20476. if (typeof format === 'number') {
  20477. index = format;
  20478. format = undefined;
  20479. }
  20480. getter = function (i) {
  20481. var m = moment().utc().set(setter, i);
  20482. return method.call(moment.fn._lang, m, format || '');
  20483. };
  20484. if (index != null) {
  20485. return getter(index);
  20486. }
  20487. else {
  20488. for (i = 0; i < count; i++) {
  20489. results.push(getter(i));
  20490. }
  20491. return results;
  20492. }
  20493. };
  20494. }
  20495. function toInt(argumentForCoercion) {
  20496. var coercedNumber = +argumentForCoercion,
  20497. value = 0;
  20498. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  20499. if (coercedNumber >= 0) {
  20500. value = Math.floor(coercedNumber);
  20501. } else {
  20502. value = Math.ceil(coercedNumber);
  20503. }
  20504. }
  20505. return value;
  20506. }
  20507. function daysInMonth(year, month) {
  20508. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  20509. }
  20510. function weeksInYear(year, dow, doy) {
  20511. return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week;
  20512. }
  20513. function daysInYear(year) {
  20514. return isLeapYear(year) ? 366 : 365;
  20515. }
  20516. function isLeapYear(year) {
  20517. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  20518. }
  20519. function checkOverflow(m) {
  20520. var overflow;
  20521. if (m._a && m._pf.overflow === -2) {
  20522. overflow =
  20523. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  20524. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  20525. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  20526. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  20527. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  20528. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  20529. -1;
  20530. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  20531. overflow = DATE;
  20532. }
  20533. m._pf.overflow = overflow;
  20534. }
  20535. }
  20536. function isValid(m) {
  20537. if (m._isValid == null) {
  20538. m._isValid = !isNaN(m._d.getTime()) &&
  20539. m._pf.overflow < 0 &&
  20540. !m._pf.empty &&
  20541. !m._pf.invalidMonth &&
  20542. !m._pf.nullInput &&
  20543. !m._pf.invalidFormat &&
  20544. !m._pf.userInvalidated;
  20545. if (m._strict) {
  20546. m._isValid = m._isValid &&
  20547. m._pf.charsLeftOver === 0 &&
  20548. m._pf.unusedTokens.length === 0;
  20549. }
  20550. }
  20551. return m._isValid;
  20552. }
  20553. function normalizeLanguage(key) {
  20554. return key ? key.toLowerCase().replace('_', '-') : key;
  20555. }
  20556. // Return a moment from input, that is local/utc/zone equivalent to model.
  20557. function makeAs(input, model) {
  20558. return model._isUTC ? moment(input).zone(model._offset || 0) :
  20559. moment(input).local();
  20560. }
  20561. /************************************
  20562. Languages
  20563. ************************************/
  20564. extend(Language.prototype, {
  20565. set : function (config) {
  20566. var prop, i;
  20567. for (i in config) {
  20568. prop = config[i];
  20569. if (typeof prop === 'function') {
  20570. this[i] = prop;
  20571. } else {
  20572. this['_' + i] = prop;
  20573. }
  20574. }
  20575. },
  20576. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  20577. months : function (m) {
  20578. return this._months[m.month()];
  20579. },
  20580. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  20581. monthsShort : function (m) {
  20582. return this._monthsShort[m.month()];
  20583. },
  20584. monthsParse : function (monthName) {
  20585. var i, mom, regex;
  20586. if (!this._monthsParse) {
  20587. this._monthsParse = [];
  20588. }
  20589. for (i = 0; i < 12; i++) {
  20590. // make the regex if we don't have it already
  20591. if (!this._monthsParse[i]) {
  20592. mom = moment.utc([2000, i]);
  20593. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  20594. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  20595. }
  20596. // test the regex
  20597. if (this._monthsParse[i].test(monthName)) {
  20598. return i;
  20599. }
  20600. }
  20601. },
  20602. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  20603. weekdays : function (m) {
  20604. return this._weekdays[m.day()];
  20605. },
  20606. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  20607. weekdaysShort : function (m) {
  20608. return this._weekdaysShort[m.day()];
  20609. },
  20610. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  20611. weekdaysMin : function (m) {
  20612. return this._weekdaysMin[m.day()];
  20613. },
  20614. weekdaysParse : function (weekdayName) {
  20615. var i, mom, regex;
  20616. if (!this._weekdaysParse) {
  20617. this._weekdaysParse = [];
  20618. }
  20619. for (i = 0; i < 7; i++) {
  20620. // make the regex if we don't have it already
  20621. if (!this._weekdaysParse[i]) {
  20622. mom = moment([2000, 1]).day(i);
  20623. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  20624. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  20625. }
  20626. // test the regex
  20627. if (this._weekdaysParse[i].test(weekdayName)) {
  20628. return i;
  20629. }
  20630. }
  20631. },
  20632. _longDateFormat : {
  20633. LT : "h:mm A",
  20634. L : "MM/DD/YYYY",
  20635. LL : "MMMM D YYYY",
  20636. LLL : "MMMM D YYYY LT",
  20637. LLLL : "dddd, MMMM D YYYY LT"
  20638. },
  20639. longDateFormat : function (key) {
  20640. var output = this._longDateFormat[key];
  20641. if (!output && this._longDateFormat[key.toUpperCase()]) {
  20642. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  20643. return val.slice(1);
  20644. });
  20645. this._longDateFormat[key] = output;
  20646. }
  20647. return output;
  20648. },
  20649. isPM : function (input) {
  20650. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  20651. // Using charAt should be more compatible.
  20652. return ((input + '').toLowerCase().charAt(0) === 'p');
  20653. },
  20654. _meridiemParse : /[ap]\.?m?\.?/i,
  20655. meridiem : function (hours, minutes, isLower) {
  20656. if (hours > 11) {
  20657. return isLower ? 'pm' : 'PM';
  20658. } else {
  20659. return isLower ? 'am' : 'AM';
  20660. }
  20661. },
  20662. _calendar : {
  20663. sameDay : '[Today at] LT',
  20664. nextDay : '[Tomorrow at] LT',
  20665. nextWeek : 'dddd [at] LT',
  20666. lastDay : '[Yesterday at] LT',
  20667. lastWeek : '[Last] dddd [at] LT',
  20668. sameElse : 'L'
  20669. },
  20670. calendar : function (key, mom) {
  20671. var output = this._calendar[key];
  20672. return typeof output === 'function' ? output.apply(mom) : output;
  20673. },
  20674. _relativeTime : {
  20675. future : "in %s",
  20676. past : "%s ago",
  20677. s : "a few seconds",
  20678. m : "a minute",
  20679. mm : "%d minutes",
  20680. h : "an hour",
  20681. hh : "%d hours",
  20682. d : "a day",
  20683. dd : "%d days",
  20684. M : "a month",
  20685. MM : "%d months",
  20686. y : "a year",
  20687. yy : "%d years"
  20688. },
  20689. relativeTime : function (number, withoutSuffix, string, isFuture) {
  20690. var output = this._relativeTime[string];
  20691. return (typeof output === 'function') ?
  20692. output(number, withoutSuffix, string, isFuture) :
  20693. output.replace(/%d/i, number);
  20694. },
  20695. pastFuture : function (diff, output) {
  20696. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  20697. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  20698. },
  20699. ordinal : function (number) {
  20700. return this._ordinal.replace("%d", number);
  20701. },
  20702. _ordinal : "%d",
  20703. preparse : function (string) {
  20704. return string;
  20705. },
  20706. postformat : function (string) {
  20707. return string;
  20708. },
  20709. week : function (mom) {
  20710. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  20711. },
  20712. _week : {
  20713. dow : 0, // Sunday is the first day of the week.
  20714. doy : 6 // The week that contains Jan 1st is the first week of the year.
  20715. },
  20716. _invalidDate: 'Invalid date',
  20717. invalidDate: function () {
  20718. return this._invalidDate;
  20719. }
  20720. });
  20721. // Loads a language definition into the `languages` cache. The function
  20722. // takes a key and optionally values. If not in the browser and no values
  20723. // are provided, it will load the language file module. As a convenience,
  20724. // this function also returns the language values.
  20725. function loadLang(key, values) {
  20726. values.abbr = key;
  20727. if (!languages[key]) {
  20728. languages[key] = new Language();
  20729. }
  20730. languages[key].set(values);
  20731. return languages[key];
  20732. }
  20733. // Remove a language from the `languages` cache. Mostly useful in tests.
  20734. function unloadLang(key) {
  20735. delete languages[key];
  20736. }
  20737. // Determines which language definition to use and returns it.
  20738. //
  20739. // With no parameters, it will return the global language. If you
  20740. // pass in a language key, such as 'en', it will return the
  20741. // definition for 'en', so long as 'en' has already been loaded using
  20742. // moment.lang.
  20743. function getLangDefinition(key) {
  20744. var i = 0, j, lang, next, split,
  20745. get = function (k) {
  20746. if (!languages[k] && hasModule) {
  20747. try {
  20748. require('./lang/' + k);
  20749. } catch (e) { }
  20750. }
  20751. return languages[k];
  20752. };
  20753. if (!key) {
  20754. return moment.fn._lang;
  20755. }
  20756. if (!isArray(key)) {
  20757. //short-circuit everything else
  20758. lang = get(key);
  20759. if (lang) {
  20760. return lang;
  20761. }
  20762. key = [key];
  20763. }
  20764. //pick the language from the array
  20765. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  20766. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  20767. while (i < key.length) {
  20768. split = normalizeLanguage(key[i]).split('-');
  20769. j = split.length;
  20770. next = normalizeLanguage(key[i + 1]);
  20771. next = next ? next.split('-') : null;
  20772. while (j > 0) {
  20773. lang = get(split.slice(0, j).join('-'));
  20774. if (lang) {
  20775. return lang;
  20776. }
  20777. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  20778. //the next array item is better than a shallower substring of this one
  20779. break;
  20780. }
  20781. j--;
  20782. }
  20783. i++;
  20784. }
  20785. return moment.fn._lang;
  20786. }
  20787. /************************************
  20788. Formatting
  20789. ************************************/
  20790. function removeFormattingTokens(input) {
  20791. if (input.match(/\[[\s\S]/)) {
  20792. return input.replace(/^\[|\]$/g, "");
  20793. }
  20794. return input.replace(/\\/g, "");
  20795. }
  20796. function makeFormatFunction(format) {
  20797. var array = format.match(formattingTokens), i, length;
  20798. for (i = 0, length = array.length; i < length; i++) {
  20799. if (formatTokenFunctions[array[i]]) {
  20800. array[i] = formatTokenFunctions[array[i]];
  20801. } else {
  20802. array[i] = removeFormattingTokens(array[i]);
  20803. }
  20804. }
  20805. return function (mom) {
  20806. var output = "";
  20807. for (i = 0; i < length; i++) {
  20808. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  20809. }
  20810. return output;
  20811. };
  20812. }
  20813. // format date using native date object
  20814. function formatMoment(m, format) {
  20815. if (!m.isValid()) {
  20816. return m.lang().invalidDate();
  20817. }
  20818. format = expandFormat(format, m.lang());
  20819. if (!formatFunctions[format]) {
  20820. formatFunctions[format] = makeFormatFunction(format);
  20821. }
  20822. return formatFunctions[format](m);
  20823. }
  20824. function expandFormat(format, lang) {
  20825. var i = 5;
  20826. function replaceLongDateFormatTokens(input) {
  20827. return lang.longDateFormat(input) || input;
  20828. }
  20829. localFormattingTokens.lastIndex = 0;
  20830. while (i >= 0 && localFormattingTokens.test(format)) {
  20831. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  20832. localFormattingTokens.lastIndex = 0;
  20833. i -= 1;
  20834. }
  20835. return format;
  20836. }
  20837. /************************************
  20838. Parsing
  20839. ************************************/
  20840. // get the regex to find the next token
  20841. function getParseRegexForToken(token, config) {
  20842. var a, strict = config._strict;
  20843. switch (token) {
  20844. case 'Q':
  20845. return parseTokenOneDigit;
  20846. case 'DDDD':
  20847. return parseTokenThreeDigits;
  20848. case 'YYYY':
  20849. case 'GGGG':
  20850. case 'gggg':
  20851. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  20852. case 'Y':
  20853. case 'G':
  20854. case 'g':
  20855. return parseTokenSignedNumber;
  20856. case 'YYYYYY':
  20857. case 'YYYYY':
  20858. case 'GGGGG':
  20859. case 'ggggg':
  20860. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  20861. case 'S':
  20862. if (strict) { return parseTokenOneDigit; }
  20863. /* falls through */
  20864. case 'SS':
  20865. if (strict) { return parseTokenTwoDigits; }
  20866. /* falls through */
  20867. case 'SSS':
  20868. if (strict) { return parseTokenThreeDigits; }
  20869. /* falls through */
  20870. case 'DDD':
  20871. return parseTokenOneToThreeDigits;
  20872. case 'MMM':
  20873. case 'MMMM':
  20874. case 'dd':
  20875. case 'ddd':
  20876. case 'dddd':
  20877. return parseTokenWord;
  20878. case 'a':
  20879. case 'A':
  20880. return getLangDefinition(config._l)._meridiemParse;
  20881. case 'X':
  20882. return parseTokenTimestampMs;
  20883. case 'Z':
  20884. case 'ZZ':
  20885. return parseTokenTimezone;
  20886. case 'T':
  20887. return parseTokenT;
  20888. case 'SSSS':
  20889. return parseTokenDigits;
  20890. case 'MM':
  20891. case 'DD':
  20892. case 'YY':
  20893. case 'GG':
  20894. case 'gg':
  20895. case 'HH':
  20896. case 'hh':
  20897. case 'mm':
  20898. case 'ss':
  20899. case 'ww':
  20900. case 'WW':
  20901. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  20902. case 'M':
  20903. case 'D':
  20904. case 'd':
  20905. case 'H':
  20906. case 'h':
  20907. case 'm':
  20908. case 's':
  20909. case 'w':
  20910. case 'W':
  20911. case 'e':
  20912. case 'E':
  20913. return parseTokenOneOrTwoDigits;
  20914. case 'Do':
  20915. return parseTokenOrdinal;
  20916. default :
  20917. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  20918. return a;
  20919. }
  20920. }
  20921. function timezoneMinutesFromString(string) {
  20922. string = string || "";
  20923. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  20924. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  20925. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  20926. minutes = +(parts[1] * 60) + toInt(parts[2]);
  20927. return parts[0] === '+' ? -minutes : minutes;
  20928. }
  20929. // function to convert string input to date
  20930. function addTimeToArrayFromToken(token, input, config) {
  20931. var a, datePartArray = config._a;
  20932. switch (token) {
  20933. // QUARTER
  20934. case 'Q':
  20935. if (input != null) {
  20936. datePartArray[MONTH] = (toInt(input) - 1) * 3;
  20937. }
  20938. break;
  20939. // MONTH
  20940. case 'M' : // fall through to MM
  20941. case 'MM' :
  20942. if (input != null) {
  20943. datePartArray[MONTH] = toInt(input) - 1;
  20944. }
  20945. break;
  20946. case 'MMM' : // fall through to MMMM
  20947. case 'MMMM' :
  20948. a = getLangDefinition(config._l).monthsParse(input);
  20949. // if we didn't find a month name, mark the date as invalid.
  20950. if (a != null) {
  20951. datePartArray[MONTH] = a;
  20952. } else {
  20953. config._pf.invalidMonth = input;
  20954. }
  20955. break;
  20956. // DAY OF MONTH
  20957. case 'D' : // fall through to DD
  20958. case 'DD' :
  20959. if (input != null) {
  20960. datePartArray[DATE] = toInt(input);
  20961. }
  20962. break;
  20963. case 'Do' :
  20964. if (input != null) {
  20965. datePartArray[DATE] = toInt(parseInt(input, 10));
  20966. }
  20967. break;
  20968. // DAY OF YEAR
  20969. case 'DDD' : // fall through to DDDD
  20970. case 'DDDD' :
  20971. if (input != null) {
  20972. config._dayOfYear = toInt(input);
  20973. }
  20974. break;
  20975. // YEAR
  20976. case 'YY' :
  20977. datePartArray[YEAR] = moment.parseTwoDigitYear(input);
  20978. break;
  20979. case 'YYYY' :
  20980. case 'YYYYY' :
  20981. case 'YYYYYY' :
  20982. datePartArray[YEAR] = toInt(input);
  20983. break;
  20984. // AM / PM
  20985. case 'a' : // fall through to A
  20986. case 'A' :
  20987. config._isPm = getLangDefinition(config._l).isPM(input);
  20988. break;
  20989. // 24 HOUR
  20990. case 'H' : // fall through to hh
  20991. case 'HH' : // fall through to hh
  20992. case 'h' : // fall through to hh
  20993. case 'hh' :
  20994. datePartArray[HOUR] = toInt(input);
  20995. break;
  20996. // MINUTE
  20997. case 'm' : // fall through to mm
  20998. case 'mm' :
  20999. datePartArray[MINUTE] = toInt(input);
  21000. break;
  21001. // SECOND
  21002. case 's' : // fall through to ss
  21003. case 'ss' :
  21004. datePartArray[SECOND] = toInt(input);
  21005. break;
  21006. // MILLISECOND
  21007. case 'S' :
  21008. case 'SS' :
  21009. case 'SSS' :
  21010. case 'SSSS' :
  21011. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  21012. break;
  21013. // UNIX TIMESTAMP WITH MS
  21014. case 'X':
  21015. config._d = new Date(parseFloat(input) * 1000);
  21016. break;
  21017. // TIMEZONE
  21018. case 'Z' : // fall through to ZZ
  21019. case 'ZZ' :
  21020. config._useUTC = true;
  21021. config._tzm = timezoneMinutesFromString(input);
  21022. break;
  21023. case 'w':
  21024. case 'ww':
  21025. case 'W':
  21026. case 'WW':
  21027. case 'd':
  21028. case 'dd':
  21029. case 'ddd':
  21030. case 'dddd':
  21031. case 'e':
  21032. case 'E':
  21033. token = token.substr(0, 1);
  21034. /* falls through */
  21035. case 'gg':
  21036. case 'gggg':
  21037. case 'GG':
  21038. case 'GGGG':
  21039. case 'GGGGG':
  21040. token = token.substr(0, 2);
  21041. if (input) {
  21042. config._w = config._w || {};
  21043. config._w[token] = input;
  21044. }
  21045. break;
  21046. }
  21047. }
  21048. // convert an array to a date.
  21049. // the array should mirror the parameters below
  21050. // note: all values past the year are optional and will default to the lowest possible value.
  21051. // [year, month, day , hour, minute, second, millisecond]
  21052. function dateFromConfig(config) {
  21053. var i, date, input = [], currentDate,
  21054. yearToUse, fixYear, w, temp, lang, weekday, week;
  21055. if (config._d) {
  21056. return;
  21057. }
  21058. currentDate = currentDateArray(config);
  21059. //compute day of the year from weeks and weekdays
  21060. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  21061. fixYear = function (val) {
  21062. var intVal = parseInt(val, 10);
  21063. return val ?
  21064. (val.length < 3 ? (intVal > 68 ? 1900 + intVal : 2000 + intVal) : intVal) :
  21065. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  21066. };
  21067. w = config._w;
  21068. if (w.GG != null || w.W != null || w.E != null) {
  21069. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  21070. }
  21071. else {
  21072. lang = getLangDefinition(config._l);
  21073. weekday = w.d != null ? parseWeekday(w.d, lang) :
  21074. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  21075. week = parseInt(w.w, 10) || 1;
  21076. //if we're parsing 'd', then the low day numbers may be next week
  21077. if (w.d != null && weekday < lang._week.dow) {
  21078. week++;
  21079. }
  21080. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  21081. }
  21082. config._a[YEAR] = temp.year;
  21083. config._dayOfYear = temp.dayOfYear;
  21084. }
  21085. //if the day of the year is set, figure out what it is
  21086. if (config._dayOfYear) {
  21087. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  21088. if (config._dayOfYear > daysInYear(yearToUse)) {
  21089. config._pf._overflowDayOfYear = true;
  21090. }
  21091. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  21092. config._a[MONTH] = date.getUTCMonth();
  21093. config._a[DATE] = date.getUTCDate();
  21094. }
  21095. // Default to current date.
  21096. // * if no year, month, day of month are given, default to today
  21097. // * if day of month is given, default month and year
  21098. // * if month is given, default only year
  21099. // * if year is given, don't default anything
  21100. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  21101. config._a[i] = input[i] = currentDate[i];
  21102. }
  21103. // Zero out whatever was not defaulted, including time
  21104. for (; i < 7; i++) {
  21105. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  21106. }
  21107. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  21108. input[HOUR] += toInt((config._tzm || 0) / 60);
  21109. input[MINUTE] += toInt((config._tzm || 0) % 60);
  21110. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  21111. }
  21112. function dateFromObject(config) {
  21113. var normalizedInput;
  21114. if (config._d) {
  21115. return;
  21116. }
  21117. normalizedInput = normalizeObjectUnits(config._i);
  21118. config._a = [
  21119. normalizedInput.year,
  21120. normalizedInput.month,
  21121. normalizedInput.day,
  21122. normalizedInput.hour,
  21123. normalizedInput.minute,
  21124. normalizedInput.second,
  21125. normalizedInput.millisecond
  21126. ];
  21127. dateFromConfig(config);
  21128. }
  21129. function currentDateArray(config) {
  21130. var now = new Date();
  21131. if (config._useUTC) {
  21132. return [
  21133. now.getUTCFullYear(),
  21134. now.getUTCMonth(),
  21135. now.getUTCDate()
  21136. ];
  21137. } else {
  21138. return [now.getFullYear(), now.getMonth(), now.getDate()];
  21139. }
  21140. }
  21141. // date from string and format string
  21142. function makeDateFromStringAndFormat(config) {
  21143. config._a = [];
  21144. config._pf.empty = true;
  21145. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  21146. var lang = getLangDefinition(config._l),
  21147. string = '' + config._i,
  21148. i, parsedInput, tokens, token, skipped,
  21149. stringLength = string.length,
  21150. totalParsedInputLength = 0;
  21151. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  21152. for (i = 0; i < tokens.length; i++) {
  21153. token = tokens[i];
  21154. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  21155. if (parsedInput) {
  21156. skipped = string.substr(0, string.indexOf(parsedInput));
  21157. if (skipped.length > 0) {
  21158. config._pf.unusedInput.push(skipped);
  21159. }
  21160. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  21161. totalParsedInputLength += parsedInput.length;
  21162. }
  21163. // don't parse if it's not a known token
  21164. if (formatTokenFunctions[token]) {
  21165. if (parsedInput) {
  21166. config._pf.empty = false;
  21167. }
  21168. else {
  21169. config._pf.unusedTokens.push(token);
  21170. }
  21171. addTimeToArrayFromToken(token, parsedInput, config);
  21172. }
  21173. else if (config._strict && !parsedInput) {
  21174. config._pf.unusedTokens.push(token);
  21175. }
  21176. }
  21177. // add remaining unparsed input length to the string
  21178. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  21179. if (string.length > 0) {
  21180. config._pf.unusedInput.push(string);
  21181. }
  21182. // handle am pm
  21183. if (config._isPm && config._a[HOUR] < 12) {
  21184. config._a[HOUR] += 12;
  21185. }
  21186. // if is 12 am, change hours to 0
  21187. if (config._isPm === false && config._a[HOUR] === 12) {
  21188. config._a[HOUR] = 0;
  21189. }
  21190. dateFromConfig(config);
  21191. checkOverflow(config);
  21192. }
  21193. function unescapeFormat(s) {
  21194. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  21195. return p1 || p2 || p3 || p4;
  21196. });
  21197. }
  21198. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  21199. function regexpEscape(s) {
  21200. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  21201. }
  21202. // date from string and array of format strings
  21203. function makeDateFromStringAndArray(config) {
  21204. var tempConfig,
  21205. bestMoment,
  21206. scoreToBeat,
  21207. i,
  21208. currentScore;
  21209. if (config._f.length === 0) {
  21210. config._pf.invalidFormat = true;
  21211. config._d = new Date(NaN);
  21212. return;
  21213. }
  21214. for (i = 0; i < config._f.length; i++) {
  21215. currentScore = 0;
  21216. tempConfig = extend({}, config);
  21217. tempConfig._pf = defaultParsingFlags();
  21218. tempConfig._f = config._f[i];
  21219. makeDateFromStringAndFormat(tempConfig);
  21220. if (!isValid(tempConfig)) {
  21221. continue;
  21222. }
  21223. // if there is any input that was not parsed add a penalty for that format
  21224. currentScore += tempConfig._pf.charsLeftOver;
  21225. //or tokens
  21226. currentScore += tempConfig._pf.unusedTokens.length * 10;
  21227. tempConfig._pf.score = currentScore;
  21228. if (scoreToBeat == null || currentScore < scoreToBeat) {
  21229. scoreToBeat = currentScore;
  21230. bestMoment = tempConfig;
  21231. }
  21232. }
  21233. extend(config, bestMoment || tempConfig);
  21234. }
  21235. // date from iso format
  21236. function makeDateFromString(config) {
  21237. var i, l,
  21238. string = config._i,
  21239. match = isoRegex.exec(string);
  21240. if (match) {
  21241. config._pf.iso = true;
  21242. for (i = 0, l = isoDates.length; i < l; i++) {
  21243. if (isoDates[i][1].exec(string)) {
  21244. // match[5] should be "T" or undefined
  21245. config._f = isoDates[i][0] + (match[6] || " ");
  21246. break;
  21247. }
  21248. }
  21249. for (i = 0, l = isoTimes.length; i < l; i++) {
  21250. if (isoTimes[i][1].exec(string)) {
  21251. config._f += isoTimes[i][0];
  21252. break;
  21253. }
  21254. }
  21255. if (string.match(parseTokenTimezone)) {
  21256. config._f += "Z";
  21257. }
  21258. makeDateFromStringAndFormat(config);
  21259. }
  21260. else {
  21261. moment.createFromInputFallback(config);
  21262. }
  21263. }
  21264. function makeDateFromInput(config) {
  21265. var input = config._i,
  21266. matched = aspNetJsonRegex.exec(input);
  21267. if (input === undefined) {
  21268. config._d = new Date();
  21269. } else if (matched) {
  21270. config._d = new Date(+matched[1]);
  21271. } else if (typeof input === 'string') {
  21272. makeDateFromString(config);
  21273. } else if (isArray(input)) {
  21274. config._a = input.slice(0);
  21275. dateFromConfig(config);
  21276. } else if (isDate(input)) {
  21277. config._d = new Date(+input);
  21278. } else if (typeof(input) === 'object') {
  21279. dateFromObject(config);
  21280. } else if (typeof(input) === 'number') {
  21281. // from milliseconds
  21282. config._d = new Date(input);
  21283. } else {
  21284. moment.createFromInputFallback(config);
  21285. }
  21286. }
  21287. function makeDate(y, m, d, h, M, s, ms) {
  21288. //can't just apply() to create a date:
  21289. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  21290. var date = new Date(y, m, d, h, M, s, ms);
  21291. //the date constructor doesn't accept years < 1970
  21292. if (y < 1970) {
  21293. date.setFullYear(y);
  21294. }
  21295. return date;
  21296. }
  21297. function makeUTCDate(y) {
  21298. var date = new Date(Date.UTC.apply(null, arguments));
  21299. if (y < 1970) {
  21300. date.setUTCFullYear(y);
  21301. }
  21302. return date;
  21303. }
  21304. function parseWeekday(input, language) {
  21305. if (typeof input === 'string') {
  21306. if (!isNaN(input)) {
  21307. input = parseInt(input, 10);
  21308. }
  21309. else {
  21310. input = language.weekdaysParse(input);
  21311. if (typeof input !== 'number') {
  21312. return null;
  21313. }
  21314. }
  21315. }
  21316. return input;
  21317. }
  21318. /************************************
  21319. Relative Time
  21320. ************************************/
  21321. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  21322. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  21323. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  21324. }
  21325. function relativeTime(milliseconds, withoutSuffix, lang) {
  21326. var seconds = round(Math.abs(milliseconds) / 1000),
  21327. minutes = round(seconds / 60),
  21328. hours = round(minutes / 60),
  21329. days = round(hours / 24),
  21330. years = round(days / 365),
  21331. args = seconds < 45 && ['s', seconds] ||
  21332. minutes === 1 && ['m'] ||
  21333. minutes < 45 && ['mm', minutes] ||
  21334. hours === 1 && ['h'] ||
  21335. hours < 22 && ['hh', hours] ||
  21336. days === 1 && ['d'] ||
  21337. days <= 25 && ['dd', days] ||
  21338. days <= 45 && ['M'] ||
  21339. days < 345 && ['MM', round(days / 30)] ||
  21340. years === 1 && ['y'] || ['yy', years];
  21341. args[2] = withoutSuffix;
  21342. args[3] = milliseconds > 0;
  21343. args[4] = lang;
  21344. return substituteTimeAgo.apply({}, args);
  21345. }
  21346. /************************************
  21347. Week of Year
  21348. ************************************/
  21349. // firstDayOfWeek 0 = sun, 6 = sat
  21350. // the day of the week that starts the week
  21351. // (usually sunday or monday)
  21352. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  21353. // the first week is the week that contains the first
  21354. // of this day of the week
  21355. // (eg. ISO weeks use thursday (4))
  21356. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  21357. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  21358. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  21359. adjustedMoment;
  21360. if (daysToDayOfWeek > end) {
  21361. daysToDayOfWeek -= 7;
  21362. }
  21363. if (daysToDayOfWeek < end - 7) {
  21364. daysToDayOfWeek += 7;
  21365. }
  21366. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  21367. return {
  21368. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  21369. year: adjustedMoment.year()
  21370. };
  21371. }
  21372. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  21373. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  21374. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  21375. weekday = weekday != null ? weekday : firstDayOfWeek;
  21376. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  21377. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  21378. return {
  21379. year: dayOfYear > 0 ? year : year - 1,
  21380. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  21381. };
  21382. }
  21383. /************************************
  21384. Top Level Functions
  21385. ************************************/
  21386. function makeMoment(config) {
  21387. var input = config._i,
  21388. format = config._f;
  21389. if (input === null || (format === undefined && input === '')) {
  21390. return moment.invalid({nullInput: true});
  21391. }
  21392. if (typeof input === 'string') {
  21393. config._i = input = getLangDefinition().preparse(input);
  21394. }
  21395. if (moment.isMoment(input)) {
  21396. config = cloneMoment(input);
  21397. config._d = new Date(+input._d);
  21398. } else if (format) {
  21399. if (isArray(format)) {
  21400. makeDateFromStringAndArray(config);
  21401. } else {
  21402. makeDateFromStringAndFormat(config);
  21403. }
  21404. } else {
  21405. makeDateFromInput(config);
  21406. }
  21407. return new Moment(config);
  21408. }
  21409. moment = function (input, format, lang, strict) {
  21410. var c;
  21411. if (typeof(lang) === "boolean") {
  21412. strict = lang;
  21413. lang = undefined;
  21414. }
  21415. // object construction must be done this way.
  21416. // https://github.com/moment/moment/issues/1423
  21417. c = {};
  21418. c._isAMomentObject = true;
  21419. c._i = input;
  21420. c._f = format;
  21421. c._l = lang;
  21422. c._strict = strict;
  21423. c._isUTC = false;
  21424. c._pf = defaultParsingFlags();
  21425. return makeMoment(c);
  21426. };
  21427. moment.suppressDeprecationWarnings = false;
  21428. moment.createFromInputFallback = deprecate(
  21429. "moment construction falls back to js Date. This is " +
  21430. "discouraged and will be removed in upcoming major " +
  21431. "release. Please refer to " +
  21432. "https://github.com/moment/moment/issues/1407 for more info.",
  21433. function (config) {
  21434. config._d = new Date(config._i);
  21435. });
  21436. // creating with utc
  21437. moment.utc = function (input, format, lang, strict) {
  21438. var c;
  21439. if (typeof(lang) === "boolean") {
  21440. strict = lang;
  21441. lang = undefined;
  21442. }
  21443. // object construction must be done this way.
  21444. // https://github.com/moment/moment/issues/1423
  21445. c = {};
  21446. c._isAMomentObject = true;
  21447. c._useUTC = true;
  21448. c._isUTC = true;
  21449. c._l = lang;
  21450. c._i = input;
  21451. c._f = format;
  21452. c._strict = strict;
  21453. c._pf = defaultParsingFlags();
  21454. return makeMoment(c).utc();
  21455. };
  21456. // creating with unix timestamp (in seconds)
  21457. moment.unix = function (input) {
  21458. return moment(input * 1000);
  21459. };
  21460. // duration
  21461. moment.duration = function (input, key) {
  21462. var duration = input,
  21463. // matching against regexp is expensive, do it on demand
  21464. match = null,
  21465. sign,
  21466. ret,
  21467. parseIso;
  21468. if (moment.isDuration(input)) {
  21469. duration = {
  21470. ms: input._milliseconds,
  21471. d: input._days,
  21472. M: input._months
  21473. };
  21474. } else if (typeof input === 'number') {
  21475. duration = {};
  21476. if (key) {
  21477. duration[key] = input;
  21478. } else {
  21479. duration.milliseconds = input;
  21480. }
  21481. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  21482. sign = (match[1] === "-") ? -1 : 1;
  21483. duration = {
  21484. y: 0,
  21485. d: toInt(match[DATE]) * sign,
  21486. h: toInt(match[HOUR]) * sign,
  21487. m: toInt(match[MINUTE]) * sign,
  21488. s: toInt(match[SECOND]) * sign,
  21489. ms: toInt(match[MILLISECOND]) * sign
  21490. };
  21491. } else if (!!(match = isoDurationRegex.exec(input))) {
  21492. sign = (match[1] === "-") ? -1 : 1;
  21493. parseIso = function (inp) {
  21494. // We'd normally use ~~inp for this, but unfortunately it also
  21495. // converts floats to ints.
  21496. // inp may be undefined, so careful calling replace on it.
  21497. var res = inp && parseFloat(inp.replace(',', '.'));
  21498. // apply sign while we're at it
  21499. return (isNaN(res) ? 0 : res) * sign;
  21500. };
  21501. duration = {
  21502. y: parseIso(match[2]),
  21503. M: parseIso(match[3]),
  21504. d: parseIso(match[4]),
  21505. h: parseIso(match[5]),
  21506. m: parseIso(match[6]),
  21507. s: parseIso(match[7]),
  21508. w: parseIso(match[8])
  21509. };
  21510. }
  21511. ret = new Duration(duration);
  21512. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  21513. ret._lang = input._lang;
  21514. }
  21515. return ret;
  21516. };
  21517. // version number
  21518. moment.version = VERSION;
  21519. // default format
  21520. moment.defaultFormat = isoFormat;
  21521. // Plugins that add properties should also add the key here (null value),
  21522. // so we can properly clone ourselves.
  21523. moment.momentProperties = momentProperties;
  21524. // This function will be called whenever a moment is mutated.
  21525. // It is intended to keep the offset in sync with the timezone.
  21526. moment.updateOffset = function () {};
  21527. // This function will load languages and then set the global language. If
  21528. // no arguments are passed in, it will simply return the current global
  21529. // language key.
  21530. moment.lang = function (key, values) {
  21531. var r;
  21532. if (!key) {
  21533. return moment.fn._lang._abbr;
  21534. }
  21535. if (values) {
  21536. loadLang(normalizeLanguage(key), values);
  21537. } else if (values === null) {
  21538. unloadLang(key);
  21539. key = 'en';
  21540. } else if (!languages[key]) {
  21541. getLangDefinition(key);
  21542. }
  21543. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  21544. return r._abbr;
  21545. };
  21546. // returns language data
  21547. moment.langData = function (key) {
  21548. if (key && key._lang && key._lang._abbr) {
  21549. key = key._lang._abbr;
  21550. }
  21551. return getLangDefinition(key);
  21552. };
  21553. // compare moment object
  21554. moment.isMoment = function (obj) {
  21555. return obj instanceof Moment ||
  21556. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  21557. };
  21558. // for typechecking Duration objects
  21559. moment.isDuration = function (obj) {
  21560. return obj instanceof Duration;
  21561. };
  21562. for (i = lists.length - 1; i >= 0; --i) {
  21563. makeList(lists[i]);
  21564. }
  21565. moment.normalizeUnits = function (units) {
  21566. return normalizeUnits(units);
  21567. };
  21568. moment.invalid = function (flags) {
  21569. var m = moment.utc(NaN);
  21570. if (flags != null) {
  21571. extend(m._pf, flags);
  21572. }
  21573. else {
  21574. m._pf.userInvalidated = true;
  21575. }
  21576. return m;
  21577. };
  21578. moment.parseZone = function () {
  21579. return moment.apply(null, arguments).parseZone();
  21580. };
  21581. moment.parseTwoDigitYear = function (input) {
  21582. return toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  21583. };
  21584. /************************************
  21585. Moment Prototype
  21586. ************************************/
  21587. extend(moment.fn = Moment.prototype, {
  21588. clone : function () {
  21589. return moment(this);
  21590. },
  21591. valueOf : function () {
  21592. return +this._d + ((this._offset || 0) * 60000);
  21593. },
  21594. unix : function () {
  21595. return Math.floor(+this / 1000);
  21596. },
  21597. toString : function () {
  21598. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  21599. },
  21600. toDate : function () {
  21601. return this._offset ? new Date(+this) : this._d;
  21602. },
  21603. toISOString : function () {
  21604. var m = moment(this).utc();
  21605. if (0 < m.year() && m.year() <= 9999) {
  21606. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  21607. } else {
  21608. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  21609. }
  21610. },
  21611. toArray : function () {
  21612. var m = this;
  21613. return [
  21614. m.year(),
  21615. m.month(),
  21616. m.date(),
  21617. m.hours(),
  21618. m.minutes(),
  21619. m.seconds(),
  21620. m.milliseconds()
  21621. ];
  21622. },
  21623. isValid : function () {
  21624. return isValid(this);
  21625. },
  21626. isDSTShifted : function () {
  21627. if (this._a) {
  21628. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  21629. }
  21630. return false;
  21631. },
  21632. parsingFlags : function () {
  21633. return extend({}, this._pf);
  21634. },
  21635. invalidAt: function () {
  21636. return this._pf.overflow;
  21637. },
  21638. utc : function () {
  21639. return this.zone(0);
  21640. },
  21641. local : function () {
  21642. this.zone(0);
  21643. this._isUTC = false;
  21644. return this;
  21645. },
  21646. format : function (inputString) {
  21647. var output = formatMoment(this, inputString || moment.defaultFormat);
  21648. return this.lang().postformat(output);
  21649. },
  21650. add : function (input, val) {
  21651. var dur;
  21652. // switch args to support add('s', 1) and add(1, 's')
  21653. if (typeof input === 'string') {
  21654. dur = moment.duration(+val, input);
  21655. } else {
  21656. dur = moment.duration(input, val);
  21657. }
  21658. addOrSubtractDurationFromMoment(this, dur, 1);
  21659. return this;
  21660. },
  21661. subtract : function (input, val) {
  21662. var dur;
  21663. // switch args to support subtract('s', 1) and subtract(1, 's')
  21664. if (typeof input === 'string') {
  21665. dur = moment.duration(+val, input);
  21666. } else {
  21667. dur = moment.duration(input, val);
  21668. }
  21669. addOrSubtractDurationFromMoment(this, dur, -1);
  21670. return this;
  21671. },
  21672. diff : function (input, units, asFloat) {
  21673. var that = makeAs(input, this),
  21674. zoneDiff = (this.zone() - that.zone()) * 6e4,
  21675. diff, output;
  21676. units = normalizeUnits(units);
  21677. if (units === 'year' || units === 'month') {
  21678. // average number of days in the months in the given dates
  21679. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  21680. // difference in months
  21681. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  21682. // adjust by taking difference in days, average number of days
  21683. // and dst in the given months.
  21684. output += ((this - moment(this).startOf('month')) -
  21685. (that - moment(that).startOf('month'))) / diff;
  21686. // same as above but with zones, to negate all dst
  21687. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  21688. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  21689. if (units === 'year') {
  21690. output = output / 12;
  21691. }
  21692. } else {
  21693. diff = (this - that);
  21694. output = units === 'second' ? diff / 1e3 : // 1000
  21695. units === 'minute' ? diff / 6e4 : // 1000 * 60
  21696. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  21697. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  21698. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  21699. diff;
  21700. }
  21701. return asFloat ? output : absRound(output);
  21702. },
  21703. from : function (time, withoutSuffix) {
  21704. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  21705. },
  21706. fromNow : function (withoutSuffix) {
  21707. return this.from(moment(), withoutSuffix);
  21708. },
  21709. calendar : function () {
  21710. // We want to compare the start of today, vs this.
  21711. // Getting start-of-today depends on whether we're zone'd or not.
  21712. var sod = makeAs(moment(), this).startOf('day'),
  21713. diff = this.diff(sod, 'days', true),
  21714. format = diff < -6 ? 'sameElse' :
  21715. diff < -1 ? 'lastWeek' :
  21716. diff < 0 ? 'lastDay' :
  21717. diff < 1 ? 'sameDay' :
  21718. diff < 2 ? 'nextDay' :
  21719. diff < 7 ? 'nextWeek' : 'sameElse';
  21720. return this.format(this.lang().calendar(format, this));
  21721. },
  21722. isLeapYear : function () {
  21723. return isLeapYear(this.year());
  21724. },
  21725. isDST : function () {
  21726. return (this.zone() < this.clone().month(0).zone() ||
  21727. this.zone() < this.clone().month(5).zone());
  21728. },
  21729. day : function (input) {
  21730. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  21731. if (input != null) {
  21732. input = parseWeekday(input, this.lang());
  21733. return this.add({ d : input - day });
  21734. } else {
  21735. return day;
  21736. }
  21737. },
  21738. month : makeAccessor('Month', true),
  21739. startOf: function (units) {
  21740. units = normalizeUnits(units);
  21741. // the following switch intentionally omits break keywords
  21742. // to utilize falling through the cases.
  21743. switch (units) {
  21744. case 'year':
  21745. this.month(0);
  21746. /* falls through */
  21747. case 'quarter':
  21748. case 'month':
  21749. this.date(1);
  21750. /* falls through */
  21751. case 'week':
  21752. case 'isoWeek':
  21753. case 'day':
  21754. this.hours(0);
  21755. /* falls through */
  21756. case 'hour':
  21757. this.minutes(0);
  21758. /* falls through */
  21759. case 'minute':
  21760. this.seconds(0);
  21761. /* falls through */
  21762. case 'second':
  21763. this.milliseconds(0);
  21764. /* falls through */
  21765. }
  21766. // weeks are a special case
  21767. if (units === 'week') {
  21768. this.weekday(0);
  21769. } else if (units === 'isoWeek') {
  21770. this.isoWeekday(1);
  21771. }
  21772. // quarters are also special
  21773. if (units === 'quarter') {
  21774. this.month(Math.floor(this.month() / 3) * 3);
  21775. }
  21776. return this;
  21777. },
  21778. endOf: function (units) {
  21779. units = normalizeUnits(units);
  21780. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  21781. },
  21782. isAfter: function (input, units) {
  21783. units = typeof units !== 'undefined' ? units : 'millisecond';
  21784. return +this.clone().startOf(units) > +moment(input).startOf(units);
  21785. },
  21786. isBefore: function (input, units) {
  21787. units = typeof units !== 'undefined' ? units : 'millisecond';
  21788. return +this.clone().startOf(units) < +moment(input).startOf(units);
  21789. },
  21790. isSame: function (input, units) {
  21791. units = units || 'ms';
  21792. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  21793. },
  21794. min: function (other) {
  21795. other = moment.apply(null, arguments);
  21796. return other < this ? this : other;
  21797. },
  21798. max: function (other) {
  21799. other = moment.apply(null, arguments);
  21800. return other > this ? this : other;
  21801. },
  21802. // keepTime = true means only change the timezone, without affecting
  21803. // the local hour. So 5:31:26 +0300 --[zone(2, true)]--> 5:31:26 +0200
  21804. // It is possible that 5:31:26 doesn't exist int zone +0200, so we
  21805. // adjust the time as needed, to be valid.
  21806. //
  21807. // Keeping the time actually adds/subtracts (one hour)
  21808. // from the actual represented time. That is why we call updateOffset
  21809. // a second time. In case it wants us to change the offset again
  21810. // _changeInProgress == true case, then we have to adjust, because
  21811. // there is no such time in the given timezone.
  21812. zone : function (input, keepTime) {
  21813. var offset = this._offset || 0;
  21814. if (input != null) {
  21815. if (typeof input === "string") {
  21816. input = timezoneMinutesFromString(input);
  21817. }
  21818. if (Math.abs(input) < 16) {
  21819. input = input * 60;
  21820. }
  21821. this._offset = input;
  21822. this._isUTC = true;
  21823. if (offset !== input) {
  21824. if (!keepTime || this._changeInProgress) {
  21825. addOrSubtractDurationFromMoment(this,
  21826. moment.duration(offset - input, 'm'), 1, false);
  21827. } else if (!this._changeInProgress) {
  21828. this._changeInProgress = true;
  21829. moment.updateOffset(this, true);
  21830. this._changeInProgress = null;
  21831. }
  21832. }
  21833. } else {
  21834. return this._isUTC ? offset : this._d.getTimezoneOffset();
  21835. }
  21836. return this;
  21837. },
  21838. zoneAbbr : function () {
  21839. return this._isUTC ? "UTC" : "";
  21840. },
  21841. zoneName : function () {
  21842. return this._isUTC ? "Coordinated Universal Time" : "";
  21843. },
  21844. parseZone : function () {
  21845. if (this._tzm) {
  21846. this.zone(this._tzm);
  21847. } else if (typeof this._i === 'string') {
  21848. this.zone(this._i);
  21849. }
  21850. return this;
  21851. },
  21852. hasAlignedHourOffset : function (input) {
  21853. if (!input) {
  21854. input = 0;
  21855. }
  21856. else {
  21857. input = moment(input).zone();
  21858. }
  21859. return (this.zone() - input) % 60 === 0;
  21860. },
  21861. daysInMonth : function () {
  21862. return daysInMonth(this.year(), this.month());
  21863. },
  21864. dayOfYear : function (input) {
  21865. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  21866. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  21867. },
  21868. quarter : function (input) {
  21869. return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3);
  21870. },
  21871. weekYear : function (input) {
  21872. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  21873. return input == null ? year : this.add("y", (input - year));
  21874. },
  21875. isoWeekYear : function (input) {
  21876. var year = weekOfYear(this, 1, 4).year;
  21877. return input == null ? year : this.add("y", (input - year));
  21878. },
  21879. week : function (input) {
  21880. var week = this.lang().week(this);
  21881. return input == null ? week : this.add("d", (input - week) * 7);
  21882. },
  21883. isoWeek : function (input) {
  21884. var week = weekOfYear(this, 1, 4).week;
  21885. return input == null ? week : this.add("d", (input - week) * 7);
  21886. },
  21887. weekday : function (input) {
  21888. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  21889. return input == null ? weekday : this.add("d", input - weekday);
  21890. },
  21891. isoWeekday : function (input) {
  21892. // behaves the same as moment#day except
  21893. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  21894. // as a setter, sunday should belong to the previous week.
  21895. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  21896. },
  21897. isoWeeksInYear : function () {
  21898. return weeksInYear(this.year(), 1, 4);
  21899. },
  21900. weeksInYear : function () {
  21901. var weekInfo = this._lang._week;
  21902. return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy);
  21903. },
  21904. get : function (units) {
  21905. units = normalizeUnits(units);
  21906. return this[units]();
  21907. },
  21908. set : function (units, value) {
  21909. units = normalizeUnits(units);
  21910. if (typeof this[units] === 'function') {
  21911. this[units](value);
  21912. }
  21913. return this;
  21914. },
  21915. // If passed a language key, it will set the language for this
  21916. // instance. Otherwise, it will return the language configuration
  21917. // variables for this instance.
  21918. lang : function (key) {
  21919. if (key === undefined) {
  21920. return this._lang;
  21921. } else {
  21922. this._lang = getLangDefinition(key);
  21923. return this;
  21924. }
  21925. }
  21926. });
  21927. function rawMonthSetter(mom, value) {
  21928. var dayOfMonth;
  21929. // TODO: Move this out of here!
  21930. if (typeof value === 'string') {
  21931. value = mom.lang().monthsParse(value);
  21932. // TODO: Another silent failure?
  21933. if (typeof value !== 'number') {
  21934. return mom;
  21935. }
  21936. }
  21937. dayOfMonth = Math.min(mom.date(),
  21938. daysInMonth(mom.year(), value));
  21939. mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth);
  21940. return mom;
  21941. }
  21942. function rawGetter(mom, unit) {
  21943. return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]();
  21944. }
  21945. function rawSetter(mom, unit, value) {
  21946. if (unit === 'Month') {
  21947. return rawMonthSetter(mom, value);
  21948. } else {
  21949. return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value);
  21950. }
  21951. }
  21952. function makeAccessor(unit, keepTime) {
  21953. return function (value) {
  21954. if (value != null) {
  21955. rawSetter(this, unit, value);
  21956. moment.updateOffset(this, keepTime);
  21957. return this;
  21958. } else {
  21959. return rawGetter(this, unit);
  21960. }
  21961. };
  21962. }
  21963. moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false);
  21964. moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false);
  21965. moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false);
  21966. // Setting the hour should keep the time, because the user explicitly
  21967. // specified which hour he wants. So trying to maintain the same hour (in
  21968. // a new timezone) makes sense. Adding/subtracting hours does not follow
  21969. // this rule.
  21970. moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true);
  21971. // moment.fn.month is defined separately
  21972. moment.fn.date = makeAccessor('Date', true);
  21973. moment.fn.dates = deprecate("dates accessor is deprecated. Use date instead.", makeAccessor('Date', true));
  21974. moment.fn.year = makeAccessor('FullYear', true);
  21975. moment.fn.years = deprecate("years accessor is deprecated. Use year instead.", makeAccessor('FullYear', true));
  21976. // add plural methods
  21977. moment.fn.days = moment.fn.day;
  21978. moment.fn.months = moment.fn.month;
  21979. moment.fn.weeks = moment.fn.week;
  21980. moment.fn.isoWeeks = moment.fn.isoWeek;
  21981. moment.fn.quarters = moment.fn.quarter;
  21982. // add aliased format methods
  21983. moment.fn.toJSON = moment.fn.toISOString;
  21984. /************************************
  21985. Duration Prototype
  21986. ************************************/
  21987. extend(moment.duration.fn = Duration.prototype, {
  21988. _bubble : function () {
  21989. var milliseconds = this._milliseconds,
  21990. days = this._days,
  21991. months = this._months,
  21992. data = this._data,
  21993. seconds, minutes, hours, years;
  21994. // The following code bubbles up values, see the tests for
  21995. // examples of what that means.
  21996. data.milliseconds = milliseconds % 1000;
  21997. seconds = absRound(milliseconds / 1000);
  21998. data.seconds = seconds % 60;
  21999. minutes = absRound(seconds / 60);
  22000. data.minutes = minutes % 60;
  22001. hours = absRound(minutes / 60);
  22002. data.hours = hours % 24;
  22003. days += absRound(hours / 24);
  22004. data.days = days % 30;
  22005. months += absRound(days / 30);
  22006. data.months = months % 12;
  22007. years = absRound(months / 12);
  22008. data.years = years;
  22009. },
  22010. weeks : function () {
  22011. return absRound(this.days() / 7);
  22012. },
  22013. valueOf : function () {
  22014. return this._milliseconds +
  22015. this._days * 864e5 +
  22016. (this._months % 12) * 2592e6 +
  22017. toInt(this._months / 12) * 31536e6;
  22018. },
  22019. humanize : function (withSuffix) {
  22020. var difference = +this,
  22021. output = relativeTime(difference, !withSuffix, this.lang());
  22022. if (withSuffix) {
  22023. output = this.lang().pastFuture(difference, output);
  22024. }
  22025. return this.lang().postformat(output);
  22026. },
  22027. add : function (input, val) {
  22028. // supports only 2.0-style add(1, 's') or add(moment)
  22029. var dur = moment.duration(input, val);
  22030. this._milliseconds += dur._milliseconds;
  22031. this._days += dur._days;
  22032. this._months += dur._months;
  22033. this._bubble();
  22034. return this;
  22035. },
  22036. subtract : function (input, val) {
  22037. var dur = moment.duration(input, val);
  22038. this._milliseconds -= dur._milliseconds;
  22039. this._days -= dur._days;
  22040. this._months -= dur._months;
  22041. this._bubble();
  22042. return this;
  22043. },
  22044. get : function (units) {
  22045. units = normalizeUnits(units);
  22046. return this[units.toLowerCase() + 's']();
  22047. },
  22048. as : function (units) {
  22049. units = normalizeUnits(units);
  22050. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  22051. },
  22052. lang : moment.fn.lang,
  22053. toIsoString : function () {
  22054. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  22055. var years = Math.abs(this.years()),
  22056. months = Math.abs(this.months()),
  22057. days = Math.abs(this.days()),
  22058. hours = Math.abs(this.hours()),
  22059. minutes = Math.abs(this.minutes()),
  22060. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  22061. if (!this.asSeconds()) {
  22062. // this is the same as C#'s (Noda) and python (isodate)...
  22063. // but not other JS (goog.date)
  22064. return 'P0D';
  22065. }
  22066. return (this.asSeconds() < 0 ? '-' : '') +
  22067. 'P' +
  22068. (years ? years + 'Y' : '') +
  22069. (months ? months + 'M' : '') +
  22070. (days ? days + 'D' : '') +
  22071. ((hours || minutes || seconds) ? 'T' : '') +
  22072. (hours ? hours + 'H' : '') +
  22073. (minutes ? minutes + 'M' : '') +
  22074. (seconds ? seconds + 'S' : '');
  22075. }
  22076. });
  22077. function makeDurationGetter(name) {
  22078. moment.duration.fn[name] = function () {
  22079. return this._data[name];
  22080. };
  22081. }
  22082. function makeDurationAsGetter(name, factor) {
  22083. moment.duration.fn['as' + name] = function () {
  22084. return +this / factor;
  22085. };
  22086. }
  22087. for (i in unitMillisecondFactors) {
  22088. if (unitMillisecondFactors.hasOwnProperty(i)) {
  22089. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  22090. makeDurationGetter(i.toLowerCase());
  22091. }
  22092. }
  22093. makeDurationAsGetter('Weeks', 6048e5);
  22094. moment.duration.fn.asMonths = function () {
  22095. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  22096. };
  22097. /************************************
  22098. Default Lang
  22099. ************************************/
  22100. // Set default language, other languages will inherit from English.
  22101. moment.lang('en', {
  22102. ordinal : function (number) {
  22103. var b = number % 10,
  22104. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  22105. (b === 1) ? 'st' :
  22106. (b === 2) ? 'nd' :
  22107. (b === 3) ? 'rd' : 'th';
  22108. return number + output;
  22109. }
  22110. });
  22111. /* EMBED_LANGUAGES */
  22112. /************************************
  22113. Exposing Moment
  22114. ************************************/
  22115. function makeGlobal(shouldDeprecate) {
  22116. /*global ender:false */
  22117. if (typeof ender !== 'undefined') {
  22118. return;
  22119. }
  22120. oldGlobalMoment = globalScope.moment;
  22121. if (shouldDeprecate) {
  22122. globalScope.moment = deprecate(
  22123. "Accessing Moment through the global scope is " +
  22124. "deprecated, and will be removed in an upcoming " +
  22125. "release.",
  22126. moment);
  22127. } else {
  22128. globalScope.moment = moment;
  22129. }
  22130. }
  22131. // CommonJS module is defined
  22132. if (hasModule) {
  22133. module.exports = moment;
  22134. } else if (typeof define === "function" && define.amd) {
  22135. define("moment", function (require, exports, module) {
  22136. if (module.config && module.config() && module.config().noGlobal === true) {
  22137. // release the global variable
  22138. globalScope.moment = oldGlobalMoment;
  22139. }
  22140. return moment;
  22141. });
  22142. makeGlobal(true);
  22143. } else {
  22144. makeGlobal();
  22145. }
  22146. }).call(this);
  22147. },{}],5:[function(require,module,exports){
  22148. /**
  22149. * Copyright 2012 Craig Campbell
  22150. *
  22151. * Licensed under the Apache License, Version 2.0 (the "License");
  22152. * you may not use this file except in compliance with the License.
  22153. * You may obtain a copy of the License at
  22154. *
  22155. * http://www.apache.org/licenses/LICENSE-2.0
  22156. *
  22157. * Unless required by applicable law or agreed to in writing, software
  22158. * distributed under the License is distributed on an "AS IS" BASIS,
  22159. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  22160. * See the License for the specific language governing permissions and
  22161. * limitations under the License.
  22162. *
  22163. * Mousetrap is a simple keyboard shortcut library for Javascript with
  22164. * no external dependencies
  22165. *
  22166. * @version 1.1.2
  22167. * @url craig.is/killing/mice
  22168. */
  22169. /**
  22170. * mapping of special keycodes to their corresponding keys
  22171. *
  22172. * everything in this dictionary cannot use keypress events
  22173. * so it has to be here to map to the correct keycodes for
  22174. * keyup/keydown events
  22175. *
  22176. * @type {Object}
  22177. */
  22178. var _MAP = {
  22179. 8: 'backspace',
  22180. 9: 'tab',
  22181. 13: 'enter',
  22182. 16: 'shift',
  22183. 17: 'ctrl',
  22184. 18: 'alt',
  22185. 20: 'capslock',
  22186. 27: 'esc',
  22187. 32: 'space',
  22188. 33: 'pageup',
  22189. 34: 'pagedown',
  22190. 35: 'end',
  22191. 36: 'home',
  22192. 37: 'left',
  22193. 38: 'up',
  22194. 39: 'right',
  22195. 40: 'down',
  22196. 45: 'ins',
  22197. 46: 'del',
  22198. 91: 'meta',
  22199. 93: 'meta',
  22200. 224: 'meta'
  22201. },
  22202. /**
  22203. * mapping for special characters so they can support
  22204. *
  22205. * this dictionary is only used incase you want to bind a
  22206. * keyup or keydown event to one of these keys
  22207. *
  22208. * @type {Object}
  22209. */
  22210. _KEYCODE_MAP = {
  22211. 106: '*',
  22212. 107: '+',
  22213. 109: '-',
  22214. 110: '.',
  22215. 111 : '/',
  22216. 186: ';',
  22217. 187: '=',
  22218. 188: ',',
  22219. 189: '-',
  22220. 190: '.',
  22221. 191: '/',
  22222. 192: '`',
  22223. 219: '[',
  22224. 220: '\\',
  22225. 221: ']',
  22226. 222: '\''
  22227. },
  22228. /**
  22229. * this is a mapping of keys that require shift on a US keypad
  22230. * back to the non shift equivelents
  22231. *
  22232. * this is so you can use keyup events with these keys
  22233. *
  22234. * note that this will only work reliably on US keyboards
  22235. *
  22236. * @type {Object}
  22237. */
  22238. _SHIFT_MAP = {
  22239. '~': '`',
  22240. '!': '1',
  22241. '@': '2',
  22242. '#': '3',
  22243. '$': '4',
  22244. '%': '5',
  22245. '^': '6',
  22246. '&': '7',
  22247. '*': '8',
  22248. '(': '9',
  22249. ')': '0',
  22250. '_': '-',
  22251. '+': '=',
  22252. ':': ';',
  22253. '\"': '\'',
  22254. '<': ',',
  22255. '>': '.',
  22256. '?': '/',
  22257. '|': '\\'
  22258. },
  22259. /**
  22260. * this is a list of special strings you can use to map
  22261. * to modifier keys when you specify your keyboard shortcuts
  22262. *
  22263. * @type {Object}
  22264. */
  22265. _SPECIAL_ALIASES = {
  22266. 'option': 'alt',
  22267. 'command': 'meta',
  22268. 'return': 'enter',
  22269. 'escape': 'esc'
  22270. },
  22271. /**
  22272. * variable to store the flipped version of _MAP from above
  22273. * needed to check if we should use keypress or not when no action
  22274. * is specified
  22275. *
  22276. * @type {Object|undefined}
  22277. */
  22278. _REVERSE_MAP,
  22279. /**
  22280. * a list of all the callbacks setup via Mousetrap.bind()
  22281. *
  22282. * @type {Object}
  22283. */
  22284. _callbacks = {},
  22285. /**
  22286. * direct map of string combinations to callbacks used for trigger()
  22287. *
  22288. * @type {Object}
  22289. */
  22290. _direct_map = {},
  22291. /**
  22292. * keeps track of what level each sequence is at since multiple
  22293. * sequences can start out with the same sequence
  22294. *
  22295. * @type {Object}
  22296. */
  22297. _sequence_levels = {},
  22298. /**
  22299. * variable to store the setTimeout call
  22300. *
  22301. * @type {null|number}
  22302. */
  22303. _reset_timer,
  22304. /**
  22305. * temporary state where we will ignore the next keyup
  22306. *
  22307. * @type {boolean|string}
  22308. */
  22309. _ignore_next_keyup = false,
  22310. /**
  22311. * are we currently inside of a sequence?
  22312. * type of action ("keyup" or "keydown" or "keypress") or false
  22313. *
  22314. * @type {boolean|string}
  22315. */
  22316. _inside_sequence = false;
  22317. /**
  22318. * loop through the f keys, f1 to f19 and add them to the map
  22319. * programatically
  22320. */
  22321. for (var i = 1; i < 20; ++i) {
  22322. _MAP[111 + i] = 'f' + i;
  22323. }
  22324. /**
  22325. * loop through to map numbers on the numeric keypad
  22326. */
  22327. for (i = 0; i <= 9; ++i) {
  22328. _MAP[i + 96] = i;
  22329. }
  22330. /**
  22331. * cross browser add event method
  22332. *
  22333. * @param {Element|HTMLDocument} object
  22334. * @param {string} type
  22335. * @param {Function} callback
  22336. * @returns void
  22337. */
  22338. function _addEvent(object, type, callback) {
  22339. if (object.addEventListener) {
  22340. return object.addEventListener(type, callback, false);
  22341. }
  22342. object.attachEvent('on' + type, callback);
  22343. }
  22344. /**
  22345. * takes the event and returns the key character
  22346. *
  22347. * @param {Event} e
  22348. * @return {string}
  22349. */
  22350. function _characterFromEvent(e) {
  22351. // for keypress events we should return the character as is
  22352. if (e.type == 'keypress') {
  22353. return String.fromCharCode(e.which);
  22354. }
  22355. // for non keypress events the special maps are needed
  22356. if (_MAP[e.which]) {
  22357. return _MAP[e.which];
  22358. }
  22359. if (_KEYCODE_MAP[e.which]) {
  22360. return _KEYCODE_MAP[e.which];
  22361. }
  22362. // if it is not in the special map
  22363. return String.fromCharCode(e.which).toLowerCase();
  22364. }
  22365. /**
  22366. * should we stop this event before firing off callbacks
  22367. *
  22368. * @param {Event} e
  22369. * @return {boolean}
  22370. */
  22371. function _stop(e) {
  22372. var element = e.target || e.srcElement,
  22373. tag_name = element.tagName;
  22374. // if the element has the class "mousetrap" then no need to stop
  22375. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  22376. return false;
  22377. }
  22378. // stop for input, select, and textarea
  22379. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  22380. }
  22381. /**
  22382. * checks if two arrays are equal
  22383. *
  22384. * @param {Array} modifiers1
  22385. * @param {Array} modifiers2
  22386. * @returns {boolean}
  22387. */
  22388. function _modifiersMatch(modifiers1, modifiers2) {
  22389. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  22390. }
  22391. /**
  22392. * resets all sequence counters except for the ones passed in
  22393. *
  22394. * @param {Object} do_not_reset
  22395. * @returns void
  22396. */
  22397. function _resetSequences(do_not_reset) {
  22398. do_not_reset = do_not_reset || {};
  22399. var active_sequences = false,
  22400. key;
  22401. for (key in _sequence_levels) {
  22402. if (do_not_reset[key]) {
  22403. active_sequences = true;
  22404. continue;
  22405. }
  22406. _sequence_levels[key] = 0;
  22407. }
  22408. if (!active_sequences) {
  22409. _inside_sequence = false;
  22410. }
  22411. }
  22412. /**
  22413. * finds all callbacks that match based on the keycode, modifiers,
  22414. * and action
  22415. *
  22416. * @param {string} character
  22417. * @param {Array} modifiers
  22418. * @param {string} action
  22419. * @param {boolean=} remove - should we remove any matches
  22420. * @param {string=} combination
  22421. * @returns {Array}
  22422. */
  22423. function _getMatches(character, modifiers, action, remove, combination) {
  22424. var i,
  22425. callback,
  22426. matches = [];
  22427. // if there are no events related to this keycode
  22428. if (!_callbacks[character]) {
  22429. return [];
  22430. }
  22431. // if a modifier key is coming up on its own we should allow it
  22432. if (action == 'keyup' && _isModifier(character)) {
  22433. modifiers = [character];
  22434. }
  22435. // loop through all callbacks for the key that was pressed
  22436. // and see if any of them match
  22437. for (i = 0; i < _callbacks[character].length; ++i) {
  22438. callback = _callbacks[character][i];
  22439. // if this is a sequence but it is not at the right level
  22440. // then move onto the next match
  22441. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  22442. continue;
  22443. }
  22444. // if the action we are looking for doesn't match the action we got
  22445. // then we should keep going
  22446. if (action != callback.action) {
  22447. continue;
  22448. }
  22449. // if this is a keypress event that means that we need to only
  22450. // look at the character, otherwise check the modifiers as
  22451. // well
  22452. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  22453. // remove is used so if you change your mind and call bind a
  22454. // second time with a new function the first one is overwritten
  22455. if (remove && callback.combo == combination) {
  22456. _callbacks[character].splice(i, 1);
  22457. }
  22458. matches.push(callback);
  22459. }
  22460. }
  22461. return matches;
  22462. }
  22463. /**
  22464. * takes a key event and figures out what the modifiers are
  22465. *
  22466. * @param {Event} e
  22467. * @returns {Array}
  22468. */
  22469. function _eventModifiers(e) {
  22470. var modifiers = [];
  22471. if (e.shiftKey) {
  22472. modifiers.push('shift');
  22473. }
  22474. if (e.altKey) {
  22475. modifiers.push('alt');
  22476. }
  22477. if (e.ctrlKey) {
  22478. modifiers.push('ctrl');
  22479. }
  22480. if (e.metaKey) {
  22481. modifiers.push('meta');
  22482. }
  22483. return modifiers;
  22484. }
  22485. /**
  22486. * actually calls the callback function
  22487. *
  22488. * if your callback function returns false this will use the jquery
  22489. * convention - prevent default and stop propogation on the event
  22490. *
  22491. * @param {Function} callback
  22492. * @param {Event} e
  22493. * @returns void
  22494. */
  22495. function _fireCallback(callback, e) {
  22496. if (callback(e) === false) {
  22497. if (e.preventDefault) {
  22498. e.preventDefault();
  22499. }
  22500. if (e.stopPropagation) {
  22501. e.stopPropagation();
  22502. }
  22503. e.returnValue = false;
  22504. e.cancelBubble = true;
  22505. }
  22506. }
  22507. /**
  22508. * handles a character key event
  22509. *
  22510. * @param {string} character
  22511. * @param {Event} e
  22512. * @returns void
  22513. */
  22514. function _handleCharacter(character, e) {
  22515. // if this event should not happen stop here
  22516. if (_stop(e)) {
  22517. return;
  22518. }
  22519. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  22520. i,
  22521. do_not_reset = {},
  22522. processed_sequence_callback = false;
  22523. // loop through matching callbacks for this key event
  22524. for (i = 0; i < callbacks.length; ++i) {
  22525. // fire for all sequence callbacks
  22526. // this is because if for example you have multiple sequences
  22527. // bound such as "g i" and "g t" they both need to fire the
  22528. // callback for matching g cause otherwise you can only ever
  22529. // match the first one
  22530. if (callbacks[i].seq) {
  22531. processed_sequence_callback = true;
  22532. // keep a list of which sequences were matches for later
  22533. do_not_reset[callbacks[i].seq] = 1;
  22534. _fireCallback(callbacks[i].callback, e);
  22535. continue;
  22536. }
  22537. // if there were no sequence matches but we are still here
  22538. // that means this is a regular match so we should fire that
  22539. if (!processed_sequence_callback && !_inside_sequence) {
  22540. _fireCallback(callbacks[i].callback, e);
  22541. }
  22542. }
  22543. // if you are inside of a sequence and the key you are pressing
  22544. // is not a modifier key then we should reset all sequences
  22545. // that were not matched by this key event
  22546. if (e.type == _inside_sequence && !_isModifier(character)) {
  22547. _resetSequences(do_not_reset);
  22548. }
  22549. }
  22550. /**
  22551. * handles a keydown event
  22552. *
  22553. * @param {Event} e
  22554. * @returns void
  22555. */
  22556. function _handleKey(e) {
  22557. // normalize e.which for key events
  22558. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  22559. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  22560. var character = _characterFromEvent(e);
  22561. // no character found then stop
  22562. if (!character) {
  22563. return;
  22564. }
  22565. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  22566. _ignore_next_keyup = false;
  22567. return;
  22568. }
  22569. _handleCharacter(character, e);
  22570. }
  22571. /**
  22572. * determines if the keycode specified is a modifier key or not
  22573. *
  22574. * @param {string} key
  22575. * @returns {boolean}
  22576. */
  22577. function _isModifier(key) {
  22578. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  22579. }
  22580. /**
  22581. * called to set a 1 second timeout on the specified sequence
  22582. *
  22583. * this is so after each key press in the sequence you have 1 second
  22584. * to press the next key before you have to start over
  22585. *
  22586. * @returns void
  22587. */
  22588. function _resetSequenceTimer() {
  22589. clearTimeout(_reset_timer);
  22590. _reset_timer = setTimeout(_resetSequences, 1000);
  22591. }
  22592. /**
  22593. * reverses the map lookup so that we can look for specific keys
  22594. * to see what can and can't use keypress
  22595. *
  22596. * @return {Object}
  22597. */
  22598. function _getReverseMap() {
  22599. if (!_REVERSE_MAP) {
  22600. _REVERSE_MAP = {};
  22601. for (var key in _MAP) {
  22602. // pull out the numeric keypad from here cause keypress should
  22603. // be able to detect the keys from the character
  22604. if (key > 95 && key < 112) {
  22605. continue;
  22606. }
  22607. if (_MAP.hasOwnProperty(key)) {
  22608. _REVERSE_MAP[_MAP[key]] = key;
  22609. }
  22610. }
  22611. }
  22612. return _REVERSE_MAP;
  22613. }
  22614. /**
  22615. * picks the best action based on the key combination
  22616. *
  22617. * @param {string} key - character for key
  22618. * @param {Array} modifiers
  22619. * @param {string=} action passed in
  22620. */
  22621. function _pickBestAction(key, modifiers, action) {
  22622. // if no action was picked in we should try to pick the one
  22623. // that we think would work best for this key
  22624. if (!action) {
  22625. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  22626. }
  22627. // modifier keys don't work as expected with keypress,
  22628. // switch to keydown
  22629. if (action == 'keypress' && modifiers.length) {
  22630. action = 'keydown';
  22631. }
  22632. return action;
  22633. }
  22634. /**
  22635. * binds a key sequence to an event
  22636. *
  22637. * @param {string} combo - combo specified in bind call
  22638. * @param {Array} keys
  22639. * @param {Function} callback
  22640. * @param {string=} action
  22641. * @returns void
  22642. */
  22643. function _bindSequence(combo, keys, callback, action) {
  22644. // start off by adding a sequence level record for this combination
  22645. // and setting the level to 0
  22646. _sequence_levels[combo] = 0;
  22647. // if there is no action pick the best one for the first key
  22648. // in the sequence
  22649. if (!action) {
  22650. action = _pickBestAction(keys[0], []);
  22651. }
  22652. /**
  22653. * callback to increase the sequence level for this sequence and reset
  22654. * all other sequences that were active
  22655. *
  22656. * @param {Event} e
  22657. * @returns void
  22658. */
  22659. var _increaseSequence = function(e) {
  22660. _inside_sequence = action;
  22661. ++_sequence_levels[combo];
  22662. _resetSequenceTimer();
  22663. },
  22664. /**
  22665. * wraps the specified callback inside of another function in order
  22666. * to reset all sequence counters as soon as this sequence is done
  22667. *
  22668. * @param {Event} e
  22669. * @returns void
  22670. */
  22671. _callbackAndReset = function(e) {
  22672. _fireCallback(callback, e);
  22673. // we should ignore the next key up if the action is key down
  22674. // or keypress. this is so if you finish a sequence and
  22675. // release the key the final key will not trigger a keyup
  22676. if (action !== 'keyup') {
  22677. _ignore_next_keyup = _characterFromEvent(e);
  22678. }
  22679. // weird race condition if a sequence ends with the key
  22680. // another sequence begins with
  22681. setTimeout(_resetSequences, 10);
  22682. },
  22683. i;
  22684. // loop through keys one at a time and bind the appropriate callback
  22685. // function. for any key leading up to the final one it should
  22686. // increase the sequence. after the final, it should reset all sequences
  22687. for (i = 0; i < keys.length; ++i) {
  22688. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  22689. }
  22690. }
  22691. /**
  22692. * binds a single keyboard combination
  22693. *
  22694. * @param {string} combination
  22695. * @param {Function} callback
  22696. * @param {string=} action
  22697. * @param {string=} sequence_name - name of sequence if part of sequence
  22698. * @param {number=} level - what part of the sequence the command is
  22699. * @returns void
  22700. */
  22701. function _bindSingle(combination, callback, action, sequence_name, level) {
  22702. // make sure multiple spaces in a row become a single space
  22703. combination = combination.replace(/\s+/g, ' ');
  22704. var sequence = combination.split(' '),
  22705. i,
  22706. key,
  22707. keys,
  22708. modifiers = [];
  22709. // if this pattern is a sequence of keys then run through this method
  22710. // to reprocess each pattern one key at a time
  22711. if (sequence.length > 1) {
  22712. return _bindSequence(combination, sequence, callback, action);
  22713. }
  22714. // take the keys from this pattern and figure out what the actual
  22715. // pattern is all about
  22716. keys = combination === '+' ? ['+'] : combination.split('+');
  22717. for (i = 0; i < keys.length; ++i) {
  22718. key = keys[i];
  22719. // normalize key names
  22720. if (_SPECIAL_ALIASES[key]) {
  22721. key = _SPECIAL_ALIASES[key];
  22722. }
  22723. // if this is not a keypress event then we should
  22724. // be smart about using shift keys
  22725. // this will only work for US keyboards however
  22726. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  22727. key = _SHIFT_MAP[key];
  22728. modifiers.push('shift');
  22729. }
  22730. // if this key is a modifier then add it to the list of modifiers
  22731. if (_isModifier(key)) {
  22732. modifiers.push(key);
  22733. }
  22734. }
  22735. // depending on what the key combination is
  22736. // we will try to pick the best event for it
  22737. action = _pickBestAction(key, modifiers, action);
  22738. // make sure to initialize array if this is the first time
  22739. // a callback is added for this key
  22740. if (!_callbacks[key]) {
  22741. _callbacks[key] = [];
  22742. }
  22743. // remove an existing match if there is one
  22744. _getMatches(key, modifiers, action, !sequence_name, combination);
  22745. // add this call back to the array
  22746. // if it is a sequence put it at the beginning
  22747. // if not put it at the end
  22748. //
  22749. // this is important because the way these are processed expects
  22750. // the sequence ones to come first
  22751. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  22752. callback: callback,
  22753. modifiers: modifiers,
  22754. action: action,
  22755. seq: sequence_name,
  22756. level: level,
  22757. combo: combination
  22758. });
  22759. }
  22760. /**
  22761. * binds multiple combinations to the same callback
  22762. *
  22763. * @param {Array} combinations
  22764. * @param {Function} callback
  22765. * @param {string|undefined} action
  22766. * @returns void
  22767. */
  22768. function _bindMultiple(combinations, callback, action) {
  22769. for (var i = 0; i < combinations.length; ++i) {
  22770. _bindSingle(combinations[i], callback, action);
  22771. }
  22772. }
  22773. // start!
  22774. _addEvent(document, 'keypress', _handleKey);
  22775. _addEvent(document, 'keydown', _handleKey);
  22776. _addEvent(document, 'keyup', _handleKey);
  22777. var mousetrap = {
  22778. /**
  22779. * binds an event to mousetrap
  22780. *
  22781. * can be a single key, a combination of keys separated with +,
  22782. * a comma separated list of keys, an array of keys, or
  22783. * a sequence of keys separated by spaces
  22784. *
  22785. * be sure to list the modifier keys first to make sure that the
  22786. * correct key ends up getting bound (the last key in the pattern)
  22787. *
  22788. * @param {string|Array} keys
  22789. * @param {Function} callback
  22790. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  22791. * @returns void
  22792. */
  22793. bind: function(keys, callback, action) {
  22794. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  22795. _direct_map[keys + ':' + action] = callback;
  22796. return this;
  22797. },
  22798. /**
  22799. * unbinds an event to mousetrap
  22800. *
  22801. * the unbinding sets the callback function of the specified key combo
  22802. * to an empty function and deletes the corresponding key in the
  22803. * _direct_map dict.
  22804. *
  22805. * the keycombo+action has to be exactly the same as
  22806. * it was defined in the bind method
  22807. *
  22808. * TODO: actually remove this from the _callbacks dictionary instead
  22809. * of binding an empty function
  22810. *
  22811. * @param {string|Array} keys
  22812. * @param {string} action
  22813. * @returns void
  22814. */
  22815. unbind: function(keys, action) {
  22816. if (_direct_map[keys + ':' + action]) {
  22817. delete _direct_map[keys + ':' + action];
  22818. this.bind(keys, function() {}, action);
  22819. }
  22820. return this;
  22821. },
  22822. /**
  22823. * triggers an event that has already been bound
  22824. *
  22825. * @param {string} keys
  22826. * @param {string=} action
  22827. * @returns void
  22828. */
  22829. trigger: function(keys, action) {
  22830. _direct_map[keys + ':' + action]();
  22831. return this;
  22832. },
  22833. /**
  22834. * resets the library back to its initial state. this is useful
  22835. * if you want to clear out the current keyboard shortcuts and bind
  22836. * new ones - for example if you switch to another page
  22837. *
  22838. * @returns void
  22839. */
  22840. reset: function() {
  22841. _callbacks = {};
  22842. _direct_map = {};
  22843. return this;
  22844. }
  22845. };
  22846. module.exports = mousetrap;
  22847. },{}]},{},[1])
  22848. (1)
  22849. });