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.

19882 lines
570 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version 0.4.0-SNAPSHOT
  8. * @date 2014-01-31
  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 Hammer;
  33. if (typeof window !== 'undefined') {
  34. // load hammer.js only when running in a browser (where window is available)
  35. Hammer = window['Hammer'] || require('hammerjs');
  36. }
  37. else {
  38. Hammer = function () {
  39. throw Error('hammer.js is only available in a browser, not in node.js.');
  40. }
  41. }
  42. var mousetrap;
  43. if (typeof window !== 'undefined') {
  44. // load mousetrap.js only when running in a browser (where window is available)
  45. mousetrap = window['mousetrap'] || require('mousetrap');
  46. }
  47. else {
  48. mousetrap = function () {
  49. throw Error('mouseTrap is only available in a browser, not in node.js.');
  50. }
  51. }
  52. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  53. // it here in that case.
  54. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  55. if(!Array.prototype.indexOf) {
  56. Array.prototype.indexOf = function(obj){
  57. for(var i = 0; i < this.length; i++){
  58. if(this[i] == obj){
  59. return i;
  60. }
  61. }
  62. return -1;
  63. };
  64. try {
  65. console.log("Warning: Ancient browser detected. Please update your browser");
  66. }
  67. catch (err) {
  68. }
  69. }
  70. // Internet Explorer 8 and older does not support Array.forEach, so we define
  71. // it here in that case.
  72. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  73. if (!Array.prototype.forEach) {
  74. Array.prototype.forEach = function(fn, scope) {
  75. for(var i = 0, len = this.length; i < len; ++i) {
  76. fn.call(scope || this, this[i], i, this);
  77. }
  78. }
  79. }
  80. // Internet Explorer 8 and older does not support Array.map, so we define it
  81. // here in that case.
  82. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  83. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  84. // Reference: http://es5.github.com/#x15.4.4.19
  85. if (!Array.prototype.map) {
  86. Array.prototype.map = function(callback, thisArg) {
  87. var T, A, k;
  88. if (this == null) {
  89. throw new TypeError(" this is null or not defined");
  90. }
  91. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  92. var O = Object(this);
  93. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  94. // 3. Let len be ToUint32(lenValue).
  95. var len = O.length >>> 0;
  96. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  97. // See: http://es5.github.com/#x9.11
  98. if (typeof callback !== "function") {
  99. throw new TypeError(callback + " is not a function");
  100. }
  101. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  102. if (thisArg) {
  103. T = thisArg;
  104. }
  105. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  106. // the standard built-in constructor with that name and len is the value of len.
  107. A = new Array(len);
  108. // 7. Let k be 0
  109. k = 0;
  110. // 8. Repeat, while k < len
  111. while(k < len) {
  112. var kValue, mappedValue;
  113. // a. Let Pk be ToString(k).
  114. // This is implicit for LHS operands of the in operator
  115. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  116. // This step can be combined with c
  117. // c. If kPresent is true, then
  118. if (k in O) {
  119. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  120. kValue = O[ k ];
  121. // ii. Let mappedValue be the result of calling the Call internal method of callback
  122. // with T as the this value and argument list containing kValue, k, and O.
  123. mappedValue = callback.call(T, kValue, k, O);
  124. // iii. Call the DefineOwnProperty internal method of A with arguments
  125. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  126. // and false.
  127. // In browsers that support Object.defineProperty, use the following:
  128. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  129. // For best browser support, use the following:
  130. A[ k ] = mappedValue;
  131. }
  132. // d. Increase k by 1.
  133. k++;
  134. }
  135. // 9. return A
  136. return A;
  137. };
  138. }
  139. // Internet Explorer 8 and older does not support Array.filter, so we define it
  140. // here in that case.
  141. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  142. if (!Array.prototype.filter) {
  143. Array.prototype.filter = function(fun /*, thisp */) {
  144. "use strict";
  145. if (this == null) {
  146. throw new TypeError();
  147. }
  148. var t = Object(this);
  149. var len = t.length >>> 0;
  150. if (typeof fun != "function") {
  151. throw new TypeError();
  152. }
  153. var res = [];
  154. var thisp = arguments[1];
  155. for (var i = 0; i < len; i++) {
  156. if (i in t) {
  157. var val = t[i]; // in case fun mutates this
  158. if (fun.call(thisp, val, i, t))
  159. res.push(val);
  160. }
  161. }
  162. return res;
  163. };
  164. }
  165. // Internet Explorer 8 and older does not support Object.keys, so we define it
  166. // here in that case.
  167. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  168. if (!Object.keys) {
  169. Object.keys = (function () {
  170. var hasOwnProperty = Object.prototype.hasOwnProperty,
  171. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  172. dontEnums = [
  173. 'toString',
  174. 'toLocaleString',
  175. 'valueOf',
  176. 'hasOwnProperty',
  177. 'isPrototypeOf',
  178. 'propertyIsEnumerable',
  179. 'constructor'
  180. ],
  181. dontEnumsLength = dontEnums.length;
  182. return function (obj) {
  183. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  184. throw new TypeError('Object.keys called on non-object');
  185. }
  186. var result = [];
  187. for (var prop in obj) {
  188. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  189. }
  190. if (hasDontEnumBug) {
  191. for (var i=0; i < dontEnumsLength; i++) {
  192. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  193. }
  194. }
  195. return result;
  196. }
  197. })()
  198. }
  199. // Internet Explorer 8 and older does not support Array.isArray,
  200. // so we define it here in that case.
  201. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  202. if(!Array.isArray) {
  203. Array.isArray = function (vArg) {
  204. return Object.prototype.toString.call(vArg) === "[object Array]";
  205. };
  206. }
  207. // Internet Explorer 8 and older does not support Function.bind,
  208. // so we define it here in that case.
  209. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  210. if (!Function.prototype.bind) {
  211. Function.prototype.bind = function (oThis) {
  212. if (typeof this !== "function") {
  213. // closest thing possible to the ECMAScript 5 internal IsCallable function
  214. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  215. }
  216. var aArgs = Array.prototype.slice.call(arguments, 1),
  217. fToBind = this,
  218. fNOP = function () {},
  219. fBound = function () {
  220. return fToBind.apply(this instanceof fNOP && oThis
  221. ? this
  222. : oThis,
  223. aArgs.concat(Array.prototype.slice.call(arguments)));
  224. };
  225. fNOP.prototype = this.prototype;
  226. fBound.prototype = new fNOP();
  227. return fBound;
  228. };
  229. }
  230. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  231. if (!Object.create) {
  232. Object.create = function (o) {
  233. if (arguments.length > 1) {
  234. throw new Error('Object.create implementation only accepts the first parameter.');
  235. }
  236. function F() {}
  237. F.prototype = o;
  238. return new F();
  239. };
  240. }
  241. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  242. if (!Function.prototype.bind) {
  243. Function.prototype.bind = function (oThis) {
  244. if (typeof this !== "function") {
  245. // closest thing possible to the ECMAScript 5 internal IsCallable function
  246. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  247. }
  248. var aArgs = Array.prototype.slice.call(arguments, 1),
  249. fToBind = this,
  250. fNOP = function () {},
  251. fBound = function () {
  252. return fToBind.apply(this instanceof fNOP && oThis
  253. ? this
  254. : oThis,
  255. aArgs.concat(Array.prototype.slice.call(arguments)));
  256. };
  257. fNOP.prototype = this.prototype;
  258. fBound.prototype = new fNOP();
  259. return fBound;
  260. };
  261. }
  262. /**
  263. * utility functions
  264. */
  265. var util = {};
  266. /**
  267. * Test whether given object is a number
  268. * @param {*} object
  269. * @return {Boolean} isNumber
  270. */
  271. util.isNumber = function isNumber(object) {
  272. return (object instanceof Number || typeof object == 'number');
  273. };
  274. /**
  275. * Test whether given object is a string
  276. * @param {*} object
  277. * @return {Boolean} isString
  278. */
  279. util.isString = function isString(object) {
  280. return (object instanceof String || typeof object == 'string');
  281. };
  282. /**
  283. * Test whether given object is a Date, or a String containing a Date
  284. * @param {Date | String} object
  285. * @return {Boolean} isDate
  286. */
  287. util.isDate = function isDate(object) {
  288. if (object instanceof Date) {
  289. return true;
  290. }
  291. else if (util.isString(object)) {
  292. // test whether this string contains a date
  293. var match = ASPDateRegex.exec(object);
  294. if (match) {
  295. return true;
  296. }
  297. else if (!isNaN(Date.parse(object))) {
  298. return true;
  299. }
  300. }
  301. return false;
  302. };
  303. /**
  304. * Test whether given object is an instance of google.visualization.DataTable
  305. * @param {*} object
  306. * @return {Boolean} isDataTable
  307. */
  308. util.isDataTable = function isDataTable(object) {
  309. return (typeof (google) !== 'undefined') &&
  310. (google.visualization) &&
  311. (google.visualization.DataTable) &&
  312. (object instanceof google.visualization.DataTable);
  313. };
  314. /**
  315. * Create a semi UUID
  316. * source: http://stackoverflow.com/a/105074/1262753
  317. * @return {String} uuid
  318. */
  319. util.randomUUID = function randomUUID () {
  320. var S4 = function () {
  321. return Math.floor(
  322. Math.random() * 0x10000 /* 65536 */
  323. ).toString(16);
  324. };
  325. return (
  326. S4() + S4() + '-' +
  327. S4() + '-' +
  328. S4() + '-' +
  329. S4() + '-' +
  330. S4() + S4() + S4()
  331. );
  332. };
  333. /**
  334. * Extend object a with the properties of object b or a series of objects
  335. * Only properties with defined values are copied
  336. * @param {Object} a
  337. * @param {... Object} b
  338. * @return {Object} a
  339. */
  340. util.extend = function (a, b) {
  341. for (var i = 1, len = arguments.length; i < len; i++) {
  342. var other = arguments[i];
  343. for (var prop in other) {
  344. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  345. a[prop] = other[prop];
  346. }
  347. }
  348. }
  349. return a;
  350. };
  351. /**
  352. * Convert an object to another type
  353. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  354. * @param {String | undefined} type Name of the type. Available types:
  355. * 'Boolean', 'Number', 'String',
  356. * 'Date', 'Moment', ISODate', 'ASPDate'.
  357. * @return {*} object
  358. * @throws Error
  359. */
  360. util.convert = function convert(object, type) {
  361. var match;
  362. if (object === undefined) {
  363. return undefined;
  364. }
  365. if (object === null) {
  366. return null;
  367. }
  368. if (!type) {
  369. return object;
  370. }
  371. if (!(typeof type === 'string') && !(type instanceof String)) {
  372. throw new Error('Type must be a string');
  373. }
  374. //noinspection FallthroughInSwitchStatementJS
  375. switch (type) {
  376. case 'boolean':
  377. case 'Boolean':
  378. return Boolean(object);
  379. case 'number':
  380. case 'Number':
  381. return Number(object.valueOf());
  382. case 'string':
  383. case 'String':
  384. return String(object);
  385. case 'Date':
  386. if (util.isNumber(object)) {
  387. return new Date(object);
  388. }
  389. if (object instanceof Date) {
  390. return new Date(object.valueOf());
  391. }
  392. else if (moment.isMoment(object)) {
  393. return new Date(object.valueOf());
  394. }
  395. if (util.isString(object)) {
  396. match = ASPDateRegex.exec(object);
  397. if (match) {
  398. // object is an ASP date
  399. return new Date(Number(match[1])); // parse number
  400. }
  401. else {
  402. return moment(object).toDate(); // parse string
  403. }
  404. }
  405. else {
  406. throw new Error(
  407. 'Cannot convert object of type ' + util.getType(object) +
  408. ' to type Date');
  409. }
  410. case 'Moment':
  411. if (util.isNumber(object)) {
  412. return moment(object);
  413. }
  414. if (object instanceof Date) {
  415. return moment(object.valueOf());
  416. }
  417. else if (moment.isMoment(object)) {
  418. return moment(object);
  419. }
  420. if (util.isString(object)) {
  421. match = ASPDateRegex.exec(object);
  422. if (match) {
  423. // object is an ASP date
  424. return moment(Number(match[1])); // parse number
  425. }
  426. else {
  427. return moment(object); // parse string
  428. }
  429. }
  430. else {
  431. throw new Error(
  432. 'Cannot convert object of type ' + util.getType(object) +
  433. ' to type Date');
  434. }
  435. case 'ISODate':
  436. if (util.isNumber(object)) {
  437. return new Date(object);
  438. }
  439. else if (object instanceof Date) {
  440. return object.toISOString();
  441. }
  442. else if (moment.isMoment(object)) {
  443. return object.toDate().toISOString();
  444. }
  445. else if (util.isString(object)) {
  446. match = ASPDateRegex.exec(object);
  447. if (match) {
  448. // object is an ASP date
  449. return new Date(Number(match[1])).toISOString(); // parse number
  450. }
  451. else {
  452. return new Date(object).toISOString(); // parse string
  453. }
  454. }
  455. else {
  456. throw new Error(
  457. 'Cannot convert object of type ' + util.getType(object) +
  458. ' to type ISODate');
  459. }
  460. case 'ASPDate':
  461. if (util.isNumber(object)) {
  462. return '/Date(' + object + ')/';
  463. }
  464. else if (object instanceof Date) {
  465. return '/Date(' + object.valueOf() + ')/';
  466. }
  467. else if (util.isString(object)) {
  468. match = ASPDateRegex.exec(object);
  469. var value;
  470. if (match) {
  471. // object is an ASP date
  472. value = new Date(Number(match[1])).valueOf(); // parse number
  473. }
  474. else {
  475. value = new Date(object).valueOf(); // parse string
  476. }
  477. return '/Date(' + value + ')/';
  478. }
  479. else {
  480. throw new Error(
  481. 'Cannot convert object of type ' + util.getType(object) +
  482. ' to type ASPDate');
  483. }
  484. default:
  485. throw new Error('Cannot convert object of type ' + util.getType(object) +
  486. ' to type "' + type + '"');
  487. }
  488. };
  489. // parse ASP.Net Date pattern,
  490. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  491. // code from http://momentjs.com/
  492. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  493. /**
  494. * Get the type of an object, for example util.getType([]) returns 'Array'
  495. * @param {*} object
  496. * @return {String} type
  497. */
  498. util.getType = function getType(object) {
  499. var type = typeof object;
  500. if (type == 'object') {
  501. if (object == null) {
  502. return 'null';
  503. }
  504. if (object instanceof Boolean) {
  505. return 'Boolean';
  506. }
  507. if (object instanceof Number) {
  508. return 'Number';
  509. }
  510. if (object instanceof String) {
  511. return 'String';
  512. }
  513. if (object instanceof Array) {
  514. return 'Array';
  515. }
  516. if (object instanceof Date) {
  517. return 'Date';
  518. }
  519. return 'Object';
  520. }
  521. else if (type == 'number') {
  522. return 'Number';
  523. }
  524. else if (type == 'boolean') {
  525. return 'Boolean';
  526. }
  527. else if (type == 'string') {
  528. return 'String';
  529. }
  530. return type;
  531. };
  532. /**
  533. * Retrieve the absolute left value of a DOM element
  534. * @param {Element} elem A dom element, for example a div
  535. * @return {number} left The absolute left position of this element
  536. * in the browser page.
  537. */
  538. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  539. var doc = document.documentElement;
  540. var body = document.body;
  541. var left = elem.offsetLeft;
  542. var e = elem.offsetParent;
  543. while (e != null && e != body && e != doc) {
  544. left += e.offsetLeft;
  545. left -= e.scrollLeft;
  546. e = e.offsetParent;
  547. }
  548. return left;
  549. };
  550. /**
  551. * Retrieve the absolute top value of a DOM element
  552. * @param {Element} elem A dom element, for example a div
  553. * @return {number} top The absolute top position of this element
  554. * in the browser page.
  555. */
  556. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  557. var doc = document.documentElement;
  558. var body = document.body;
  559. var top = elem.offsetTop;
  560. var e = elem.offsetParent;
  561. while (e != null && e != body && e != doc) {
  562. top += e.offsetTop;
  563. top -= e.scrollTop;
  564. e = e.offsetParent;
  565. }
  566. return top;
  567. };
  568. /**
  569. * Get the absolute, vertical mouse position from an event.
  570. * @param {Event} event
  571. * @return {Number} pageY
  572. */
  573. util.getPageY = function getPageY (event) {
  574. if ('pageY' in event) {
  575. return event.pageY;
  576. }
  577. else {
  578. var clientY;
  579. if (('targetTouches' in event) && event.targetTouches.length) {
  580. clientY = event.targetTouches[0].clientY;
  581. }
  582. else {
  583. clientY = event.clientY;
  584. }
  585. var doc = document.documentElement;
  586. var body = document.body;
  587. return clientY +
  588. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  589. ( doc && doc.clientTop || body && body.clientTop || 0 );
  590. }
  591. };
  592. /**
  593. * Get the absolute, horizontal mouse position from an event.
  594. * @param {Event} event
  595. * @return {Number} pageX
  596. */
  597. util.getPageX = function getPageX (event) {
  598. if ('pageY' in event) {
  599. return event.pageX;
  600. }
  601. else {
  602. var clientX;
  603. if (('targetTouches' in event) && event.targetTouches.length) {
  604. clientX = event.targetTouches[0].clientX;
  605. }
  606. else {
  607. clientX = event.clientX;
  608. }
  609. var doc = document.documentElement;
  610. var body = document.body;
  611. return clientX +
  612. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  613. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  614. }
  615. };
  616. /**
  617. * add a className to the given elements style
  618. * @param {Element} elem
  619. * @param {String} className
  620. */
  621. util.addClassName = function addClassName(elem, className) {
  622. var classes = elem.className.split(' ');
  623. if (classes.indexOf(className) == -1) {
  624. classes.push(className); // add the class to the array
  625. elem.className = classes.join(' ');
  626. }
  627. };
  628. /**
  629. * add a className to the given elements style
  630. * @param {Element} elem
  631. * @param {String} className
  632. */
  633. util.removeClassName = function removeClassname(elem, className) {
  634. var classes = elem.className.split(' ');
  635. var index = classes.indexOf(className);
  636. if (index != -1) {
  637. classes.splice(index, 1); // remove the class from the array
  638. elem.className = classes.join(' ');
  639. }
  640. };
  641. /**
  642. * For each method for both arrays and objects.
  643. * In case of an array, the built-in Array.forEach() is applied.
  644. * In case of an Object, the method loops over all properties of the object.
  645. * @param {Object | Array} object An Object or Array
  646. * @param {function} callback Callback method, called for each item in
  647. * the object or array with three parameters:
  648. * callback(value, index, object)
  649. */
  650. util.forEach = function forEach (object, callback) {
  651. var i,
  652. len;
  653. if (object instanceof Array) {
  654. // array
  655. for (i = 0, len = object.length; i < len; i++) {
  656. callback(object[i], i, object);
  657. }
  658. }
  659. else {
  660. // object
  661. for (i in object) {
  662. if (object.hasOwnProperty(i)) {
  663. callback(object[i], i, object);
  664. }
  665. }
  666. }
  667. };
  668. /**
  669. * Update a property in an object
  670. * @param {Object} object
  671. * @param {String} key
  672. * @param {*} value
  673. * @return {Boolean} changed
  674. */
  675. util.updateProperty = function updateProp (object, key, value) {
  676. if (object[key] !== value) {
  677. object[key] = value;
  678. return true;
  679. }
  680. else {
  681. return false;
  682. }
  683. };
  684. /**
  685. * Add and event listener. Works for all browsers
  686. * @param {Element} element An html element
  687. * @param {string} action The action, for example "click",
  688. * without the prefix "on"
  689. * @param {function} listener The callback function to be executed
  690. * @param {boolean} [useCapture]
  691. */
  692. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  693. if (element.addEventListener) {
  694. if (useCapture === undefined)
  695. useCapture = false;
  696. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  697. action = "DOMMouseScroll"; // For Firefox
  698. }
  699. element.addEventListener(action, listener, useCapture);
  700. } else {
  701. element.attachEvent("on" + action, listener); // IE browsers
  702. }
  703. };
  704. /**
  705. * Remove an event listener from an element
  706. * @param {Element} element An html dom element
  707. * @param {string} action The name of the event, for example "mousedown"
  708. * @param {function} listener The listener function
  709. * @param {boolean} [useCapture]
  710. */
  711. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  712. if (element.removeEventListener) {
  713. // non-IE browsers
  714. if (useCapture === undefined)
  715. useCapture = false;
  716. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  717. action = "DOMMouseScroll"; // For Firefox
  718. }
  719. element.removeEventListener(action, listener, useCapture);
  720. } else {
  721. // IE browsers
  722. element.detachEvent("on" + action, listener);
  723. }
  724. };
  725. /**
  726. * Get HTML element which is the target of the event
  727. * @param {Event} event
  728. * @return {Element} target element
  729. */
  730. util.getTarget = function getTarget(event) {
  731. // code from http://www.quirksmode.org/js/events_properties.html
  732. if (!event) {
  733. event = window.event;
  734. }
  735. var target;
  736. if (event.target) {
  737. target = event.target;
  738. }
  739. else if (event.srcElement) {
  740. target = event.srcElement;
  741. }
  742. if (target.nodeType != undefined && target.nodeType == 3) {
  743. // defeat Safari bug
  744. target = target.parentNode;
  745. }
  746. return target;
  747. };
  748. /**
  749. * Stop event propagation
  750. */
  751. util.stopPropagation = function stopPropagation(event) {
  752. if (!event)
  753. event = window.event;
  754. if (event.stopPropagation) {
  755. event.stopPropagation(); // non-IE browsers
  756. }
  757. else {
  758. event.cancelBubble = true; // IE browsers
  759. }
  760. };
  761. /**
  762. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  763. * @param {Element} element
  764. * @param {Event} event
  765. */
  766. util.fakeGesture = function fakeGesture (element, event) {
  767. var eventType = null;
  768. // for hammer.js 1.0.5
  769. return Hammer.event.collectEventData(this, eventType, event);
  770. // for hammer.js 1.0.6
  771. //var touches = Hammer.event.getTouchList(event, eventType);
  772. //return Hammer.event.collectEventData(this, eventType, touches, event);
  773. };
  774. /**
  775. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  776. */
  777. util.preventDefault = function preventDefault (event) {
  778. if (!event)
  779. event = window.event;
  780. if (event.preventDefault) {
  781. event.preventDefault(); // non-IE browsers
  782. }
  783. else {
  784. event.returnValue = false; // IE browsers
  785. }
  786. };
  787. util.option = {};
  788. /**
  789. * Convert a value into a boolean
  790. * @param {Boolean | function | undefined} value
  791. * @param {Boolean} [defaultValue]
  792. * @returns {Boolean} bool
  793. */
  794. util.option.asBoolean = function (value, defaultValue) {
  795. if (typeof value == 'function') {
  796. value = value();
  797. }
  798. if (value != null) {
  799. return (value != false);
  800. }
  801. return defaultValue || null;
  802. };
  803. /**
  804. * Convert a value into a number
  805. * @param {Boolean | function | undefined} value
  806. * @param {Number} [defaultValue]
  807. * @returns {Number} number
  808. */
  809. util.option.asNumber = function (value, defaultValue) {
  810. if (typeof value == 'function') {
  811. value = value();
  812. }
  813. if (value != null) {
  814. return Number(value) || defaultValue || null;
  815. }
  816. return defaultValue || null;
  817. };
  818. /**
  819. * Convert a value into a string
  820. * @param {String | function | undefined} value
  821. * @param {String} [defaultValue]
  822. * @returns {String} str
  823. */
  824. util.option.asString = function (value, defaultValue) {
  825. if (typeof value == 'function') {
  826. value = value();
  827. }
  828. if (value != null) {
  829. return String(value);
  830. }
  831. return defaultValue || null;
  832. };
  833. /**
  834. * Convert a size or location into a string with pixels or a percentage
  835. * @param {String | Number | function | undefined} value
  836. * @param {String} [defaultValue]
  837. * @returns {String} size
  838. */
  839. util.option.asSize = function (value, defaultValue) {
  840. if (typeof value == 'function') {
  841. value = value();
  842. }
  843. if (util.isString(value)) {
  844. return value;
  845. }
  846. else if (util.isNumber(value)) {
  847. return value + 'px';
  848. }
  849. else {
  850. return defaultValue || null;
  851. }
  852. };
  853. /**
  854. * Convert a value into a DOM element
  855. * @param {HTMLElement | function | undefined} value
  856. * @param {HTMLElement} [defaultValue]
  857. * @returns {HTMLElement | null} dom
  858. */
  859. util.option.asElement = function (value, defaultValue) {
  860. if (typeof value == 'function') {
  861. value = value();
  862. }
  863. return value || defaultValue || null;
  864. };
  865. /**
  866. * Compare two numbers and return the lowest, non-negative number.
  867. *
  868. * @param {number} number1
  869. * @param {number} number2
  870. * @returns {number} | number1 or number2, the lowest positive number. If both negative, return -1
  871. */
  872. util.getLowestPositiveNumber = function(number1,number2) {
  873. if (number1 >= 0) {
  874. if (number2 >= 0) {
  875. return (number1 < number2) ? number1 : number2;
  876. }
  877. else {
  878. return number1;
  879. }
  880. }
  881. else {
  882. if (number2 >= 0) {
  883. return number2;
  884. }
  885. else {
  886. return -1;
  887. }
  888. }
  889. }
  890. /**
  891. * Event listener (singleton)
  892. */
  893. // TODO: replace usage of the event listener for the EventBus
  894. var events = {
  895. 'listeners': [],
  896. /**
  897. * Find a single listener by its object
  898. * @param {Object} object
  899. * @return {Number} index -1 when not found
  900. */
  901. 'indexOf': function (object) {
  902. var listeners = this.listeners;
  903. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  904. var listener = listeners[i];
  905. if (listener && listener.object == object) {
  906. return i;
  907. }
  908. }
  909. return -1;
  910. },
  911. /**
  912. * Add an event listener
  913. * @param {Object} object
  914. * @param {String} event The name of an event, for example 'select'
  915. * @param {function} callback The callback method, called when the
  916. * event takes place
  917. */
  918. 'addListener': function (object, event, callback) {
  919. var index = this.indexOf(object);
  920. var listener = this.listeners[index];
  921. if (!listener) {
  922. listener = {
  923. 'object': object,
  924. 'events': {}
  925. };
  926. this.listeners.push(listener);
  927. }
  928. var callbacks = listener.events[event];
  929. if (!callbacks) {
  930. callbacks = [];
  931. listener.events[event] = callbacks;
  932. }
  933. // add the callback if it does not yet exist
  934. if (callbacks.indexOf(callback) == -1) {
  935. callbacks.push(callback);
  936. }
  937. },
  938. /**
  939. * Remove an event listener
  940. * @param {Object} object
  941. * @param {String} event The name of an event, for example 'select'
  942. * @param {function} callback The registered callback method
  943. */
  944. 'removeListener': function (object, event, callback) {
  945. var index = this.indexOf(object);
  946. var listener = this.listeners[index];
  947. if (listener) {
  948. var callbacks = listener.events[event];
  949. if (callbacks) {
  950. index = callbacks.indexOf(callback);
  951. if (index != -1) {
  952. callbacks.splice(index, 1);
  953. }
  954. // remove the array when empty
  955. if (callbacks.length == 0) {
  956. delete listener.events[event];
  957. }
  958. }
  959. // count the number of registered events. remove listener when empty
  960. var count = 0;
  961. var events = listener.events;
  962. for (var e in events) {
  963. if (events.hasOwnProperty(e)) {
  964. count++;
  965. }
  966. }
  967. if (count == 0) {
  968. delete this.listeners[index];
  969. }
  970. }
  971. },
  972. /**
  973. * Remove all registered event listeners
  974. */
  975. 'removeAllListeners': function () {
  976. this.listeners = [];
  977. },
  978. /**
  979. * Trigger an event. All registered event handlers will be called
  980. * @param {Object} object
  981. * @param {String} event
  982. * @param {Object} properties (optional)
  983. */
  984. 'trigger': function (object, event, properties) {
  985. var index = this.indexOf(object);
  986. var listener = this.listeners[index];
  987. if (listener) {
  988. var callbacks = listener.events[event];
  989. if (callbacks) {
  990. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  991. callbacks[i](properties);
  992. }
  993. }
  994. }
  995. }
  996. };
  997. /**
  998. * An event bus can be used to emit events, and to subscribe to events
  999. * @constructor EventBus
  1000. */
  1001. function EventBus() {
  1002. this.subscriptions = [];
  1003. }
  1004. /**
  1005. * Subscribe to an event
  1006. * @param {String | RegExp} event The event can be a regular expression, or
  1007. * a string with wildcards, like 'server.*'.
  1008. * @param {function} callback. Callback are called with three parameters:
  1009. * {String} event, {*} [data], {*} [source]
  1010. * @param {*} [target]
  1011. * @returns {String} id A subscription id
  1012. */
  1013. EventBus.prototype.on = function (event, callback, target) {
  1014. var regexp = (event instanceof RegExp) ?
  1015. event :
  1016. new RegExp(event.replace('*', '\\w+'));
  1017. var subscription = {
  1018. id: util.randomUUID(),
  1019. event: event,
  1020. regexp: regexp,
  1021. callback: (typeof callback === 'function') ? callback : null,
  1022. target: target
  1023. };
  1024. this.subscriptions.push(subscription);
  1025. return subscription.id;
  1026. };
  1027. /**
  1028. * Unsubscribe from an event
  1029. * @param {String | Object} filter Filter for subscriptions to be removed
  1030. * Filter can be a string containing a
  1031. * subscription id, or an object containing
  1032. * one or more of the fields id, event,
  1033. * callback, and target.
  1034. */
  1035. EventBus.prototype.off = function (filter) {
  1036. var i = 0;
  1037. while (i < this.subscriptions.length) {
  1038. var subscription = this.subscriptions[i];
  1039. var match = true;
  1040. if (filter instanceof Object) {
  1041. // filter is an object. All fields must match
  1042. for (var prop in filter) {
  1043. if (filter.hasOwnProperty(prop)) {
  1044. if (filter[prop] !== subscription[prop]) {
  1045. match = false;
  1046. }
  1047. }
  1048. }
  1049. }
  1050. else {
  1051. // filter is a string, filter on id
  1052. match = (subscription.id == filter);
  1053. }
  1054. if (match) {
  1055. this.subscriptions.splice(i, 1);
  1056. }
  1057. else {
  1058. i++;
  1059. }
  1060. }
  1061. };
  1062. /**
  1063. * Emit an event
  1064. * @param {String} event
  1065. * @param {*} [data]
  1066. * @param {*} [source]
  1067. */
  1068. EventBus.prototype.emit = function (event, data, source) {
  1069. for (var i =0; i < this.subscriptions.length; i++) {
  1070. var subscription = this.subscriptions[i];
  1071. if (subscription.regexp.test(event)) {
  1072. if (subscription.callback) {
  1073. subscription.callback(event, data, source);
  1074. }
  1075. }
  1076. }
  1077. };
  1078. /**
  1079. * DataSet
  1080. *
  1081. * Usage:
  1082. * var dataSet = new DataSet({
  1083. * fieldId: '_id',
  1084. * convert: {
  1085. * // ...
  1086. * }
  1087. * });
  1088. *
  1089. * dataSet.add(item);
  1090. * dataSet.add(data);
  1091. * dataSet.update(item);
  1092. * dataSet.update(data);
  1093. * dataSet.remove(id);
  1094. * dataSet.remove(ids);
  1095. * var data = dataSet.get();
  1096. * var data = dataSet.get(id);
  1097. * var data = dataSet.get(ids);
  1098. * var data = dataSet.get(ids, options, data);
  1099. * dataSet.clear();
  1100. *
  1101. * A data set can:
  1102. * - add/remove/update data
  1103. * - gives triggers upon changes in the data
  1104. * - can import/export data in various data formats
  1105. *
  1106. * @param {Object} [options] Available options:
  1107. * {String} fieldId Field name of the id in the
  1108. * items, 'id' by default.
  1109. * {Object.<String, String} convert
  1110. * A map with field names as key,
  1111. * and the field type as value.
  1112. * @constructor DataSet
  1113. */
  1114. // TODO: add a DataSet constructor DataSet(data, options)
  1115. function DataSet (options) {
  1116. this.id = util.randomUUID();
  1117. this.options = options || {};
  1118. this.data = {}; // map with data indexed by id
  1119. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1120. this.convert = {}; // field types by field name
  1121. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1122. if (this.options.convert) {
  1123. for (var field in this.options.convert) {
  1124. if (this.options.convert.hasOwnProperty(field)) {
  1125. var value = this.options.convert[field];
  1126. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1127. this.convert[field] = 'Date';
  1128. }
  1129. else {
  1130. this.convert[field] = value;
  1131. }
  1132. }
  1133. }
  1134. }
  1135. // event subscribers
  1136. this.subscribers = {};
  1137. this.internalIds = {}; // internally generated id's
  1138. }
  1139. /**
  1140. * Subscribe to an event, add an event listener
  1141. * @param {String} event Event name. Available events: 'put', 'update',
  1142. * 'remove'
  1143. * @param {function} callback Callback method. Called with three parameters:
  1144. * {String} event
  1145. * {Object | null} params
  1146. * {String | Number} senderId
  1147. */
  1148. DataSet.prototype.subscribe = function (event, callback) {
  1149. var subscribers = this.subscribers[event];
  1150. if (!subscribers) {
  1151. subscribers = [];
  1152. this.subscribers[event] = subscribers;
  1153. }
  1154. subscribers.push({
  1155. callback: callback
  1156. });
  1157. };
  1158. /**
  1159. * Unsubscribe from an event, remove an event listener
  1160. * @param {String} event
  1161. * @param {function} callback
  1162. */
  1163. DataSet.prototype.unsubscribe = function (event, callback) {
  1164. var subscribers = this.subscribers[event];
  1165. if (subscribers) {
  1166. this.subscribers[event] = subscribers.filter(function (listener) {
  1167. return (listener.callback != callback);
  1168. });
  1169. }
  1170. };
  1171. /**
  1172. * Trigger an event
  1173. * @param {String} event
  1174. * @param {Object | null} params
  1175. * @param {String} [senderId] Optional id of the sender.
  1176. * @private
  1177. */
  1178. DataSet.prototype._trigger = function (event, params, senderId) {
  1179. if (event == '*') {
  1180. throw new Error('Cannot trigger event *');
  1181. }
  1182. var subscribers = [];
  1183. if (event in this.subscribers) {
  1184. subscribers = subscribers.concat(this.subscribers[event]);
  1185. }
  1186. if ('*' in this.subscribers) {
  1187. subscribers = subscribers.concat(this.subscribers['*']);
  1188. }
  1189. for (var i = 0; i < subscribers.length; i++) {
  1190. var subscriber = subscribers[i];
  1191. if (subscriber.callback) {
  1192. subscriber.callback(event, params, senderId || null);
  1193. }
  1194. }
  1195. };
  1196. /**
  1197. * Add data.
  1198. * Adding an item will fail when there already is an item with the same id.
  1199. * @param {Object | Array | DataTable} data
  1200. * @param {String} [senderId] Optional sender id
  1201. * @return {Array} addedIds Array with the ids of the added items
  1202. */
  1203. DataSet.prototype.add = function (data, senderId) {
  1204. var addedIds = [],
  1205. id,
  1206. me = this;
  1207. if (data instanceof Array) {
  1208. // Array
  1209. for (var i = 0, len = data.length; i < len; i++) {
  1210. id = me._addItem(data[i]);
  1211. addedIds.push(id);
  1212. }
  1213. }
  1214. else if (util.isDataTable(data)) {
  1215. // Google DataTable
  1216. var columns = this._getColumnNames(data);
  1217. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1218. var item = {};
  1219. for (var col = 0, cols = columns.length; col < cols; col++) {
  1220. var field = columns[col];
  1221. item[field] = data.getValue(row, col);
  1222. }
  1223. id = me._addItem(item);
  1224. addedIds.push(id);
  1225. }
  1226. }
  1227. else if (data instanceof Object) {
  1228. // Single item
  1229. id = me._addItem(data);
  1230. addedIds.push(id);
  1231. }
  1232. else {
  1233. throw new Error('Unknown dataType');
  1234. }
  1235. if (addedIds.length) {
  1236. this._trigger('add', {items: addedIds}, senderId);
  1237. }
  1238. return addedIds;
  1239. };
  1240. /**
  1241. * Update existing items. When an item does not exist, it will be created
  1242. * @param {Object | Array | DataTable} data
  1243. * @param {String} [senderId] Optional sender id
  1244. * @return {Array} updatedIds The ids of the added or updated items
  1245. */
  1246. DataSet.prototype.update = function (data, senderId) {
  1247. var addedIds = [],
  1248. updatedIds = [],
  1249. me = this,
  1250. fieldId = me.fieldId;
  1251. var addOrUpdate = function (item) {
  1252. var id = item[fieldId];
  1253. if (me.data[id]) {
  1254. // update item
  1255. id = me._updateItem(item);
  1256. updatedIds.push(id);
  1257. }
  1258. else {
  1259. // add new item
  1260. id = me._addItem(item);
  1261. addedIds.push(id);
  1262. }
  1263. };
  1264. if (data instanceof Array) {
  1265. // Array
  1266. for (var i = 0, len = data.length; i < len; i++) {
  1267. addOrUpdate(data[i]);
  1268. }
  1269. }
  1270. else if (util.isDataTable(data)) {
  1271. // Google DataTable
  1272. var columns = this._getColumnNames(data);
  1273. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1274. var item = {};
  1275. for (var col = 0, cols = columns.length; col < cols; col++) {
  1276. var field = columns[col];
  1277. item[field] = data.getValue(row, col);
  1278. }
  1279. addOrUpdate(item);
  1280. }
  1281. }
  1282. else if (data instanceof Object) {
  1283. // Single item
  1284. addOrUpdate(data);
  1285. }
  1286. else {
  1287. throw new Error('Unknown dataType');
  1288. }
  1289. if (addedIds.length) {
  1290. this._trigger('add', {items: addedIds}, senderId);
  1291. }
  1292. if (updatedIds.length) {
  1293. this._trigger('update', {items: updatedIds}, senderId);
  1294. }
  1295. return addedIds.concat(updatedIds);
  1296. };
  1297. /**
  1298. * Get a data item or multiple items.
  1299. *
  1300. * Usage:
  1301. *
  1302. * get()
  1303. * get(options: Object)
  1304. * get(options: Object, data: Array | DataTable)
  1305. *
  1306. * get(id: Number | String)
  1307. * get(id: Number | String, options: Object)
  1308. * get(id: Number | String, options: Object, data: Array | DataTable)
  1309. *
  1310. * get(ids: Number[] | String[])
  1311. * get(ids: Number[] | String[], options: Object)
  1312. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1313. *
  1314. * Where:
  1315. *
  1316. * {Number | String} id The id of an item
  1317. * {Number[] | String{}} ids An array with ids of items
  1318. * {Object} options An Object with options. Available options:
  1319. * {String} [type] Type of data to be returned. Can
  1320. * be 'DataTable' or 'Array' (default)
  1321. * {Object.<String, String>} [convert]
  1322. * {String[]} [fields] field names to be returned
  1323. * {function} [filter] filter items
  1324. * {String | function} [order] Order the items by
  1325. * a field name or custom sort function.
  1326. * {Array | DataTable} [data] If provided, items will be appended to this
  1327. * array or table. Required in case of Google
  1328. * DataTable.
  1329. *
  1330. * @throws Error
  1331. */
  1332. DataSet.prototype.get = function (args) {
  1333. var me = this;
  1334. var globalShowInternalIds = this.showInternalIds;
  1335. // parse the arguments
  1336. var id, ids, options, data;
  1337. var firstType = util.getType(arguments[0]);
  1338. if (firstType == 'String' || firstType == 'Number') {
  1339. // get(id [, options] [, data])
  1340. id = arguments[0];
  1341. options = arguments[1];
  1342. data = arguments[2];
  1343. }
  1344. else if (firstType == 'Array') {
  1345. // get(ids [, options] [, data])
  1346. ids = arguments[0];
  1347. options = arguments[1];
  1348. data = arguments[2];
  1349. }
  1350. else {
  1351. // get([, options] [, data])
  1352. options = arguments[0];
  1353. data = arguments[1];
  1354. }
  1355. // determine the return type
  1356. var type;
  1357. if (options && options.type) {
  1358. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1359. if (data && (type != util.getType(data))) {
  1360. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1361. 'does not correspond with specified options.type (' + options.type + ')');
  1362. }
  1363. if (type == 'DataTable' && !util.isDataTable(data)) {
  1364. throw new Error('Parameter "data" must be a DataTable ' +
  1365. 'when options.type is "DataTable"');
  1366. }
  1367. }
  1368. else if (data) {
  1369. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1370. }
  1371. else {
  1372. type = 'Array';
  1373. }
  1374. // we allow the setting of this value for a single get request.
  1375. if (options != undefined) {
  1376. if (options.showInternalIds != undefined) {
  1377. this.showInternalIds = options.showInternalIds;
  1378. }
  1379. }
  1380. // build options
  1381. var convert = options && options.convert || this.options.convert;
  1382. var filter = options && options.filter;
  1383. var items = [], item, itemId, i, len;
  1384. // convert items
  1385. if (id != undefined) {
  1386. // return a single item
  1387. item = me._getItem(id, convert);
  1388. if (filter && !filter(item)) {
  1389. item = null;
  1390. }
  1391. }
  1392. else if (ids != undefined) {
  1393. // return a subset of items
  1394. for (i = 0, len = ids.length; i < len; i++) {
  1395. item = me._getItem(ids[i], convert);
  1396. if (!filter || filter(item)) {
  1397. items.push(item);
  1398. }
  1399. }
  1400. }
  1401. else {
  1402. // return all items
  1403. for (itemId in this.data) {
  1404. if (this.data.hasOwnProperty(itemId)) {
  1405. item = me._getItem(itemId, convert);
  1406. if (!filter || filter(item)) {
  1407. items.push(item);
  1408. }
  1409. }
  1410. }
  1411. }
  1412. // restore the global value of showInternalIds
  1413. this.showInternalIds = globalShowInternalIds;
  1414. // order the results
  1415. if (options && options.order && id == undefined) {
  1416. this._sort(items, options.order);
  1417. }
  1418. // filter fields of the items
  1419. if (options && options.fields) {
  1420. var fields = options.fields;
  1421. if (id != undefined) {
  1422. item = this._filterFields(item, fields);
  1423. }
  1424. else {
  1425. for (i = 0, len = items.length; i < len; i++) {
  1426. items[i] = this._filterFields(items[i], fields);
  1427. }
  1428. }
  1429. }
  1430. // return the results
  1431. if (type == 'DataTable') {
  1432. var columns = this._getColumnNames(data);
  1433. if (id != undefined) {
  1434. // append a single item to the data table
  1435. me._appendRow(data, columns, item);
  1436. }
  1437. else {
  1438. // copy the items to the provided data table
  1439. for (i = 0, len = items.length; i < len; i++) {
  1440. me._appendRow(data, columns, items[i]);
  1441. }
  1442. }
  1443. return data;
  1444. }
  1445. else {
  1446. // return an array
  1447. if (id != undefined) {
  1448. // a single item
  1449. return item;
  1450. }
  1451. else {
  1452. // multiple items
  1453. if (data) {
  1454. // copy the items to the provided array
  1455. for (i = 0, len = items.length; i < len; i++) {
  1456. data.push(items[i]);
  1457. }
  1458. return data;
  1459. }
  1460. else {
  1461. // just return our array
  1462. return items;
  1463. }
  1464. }
  1465. }
  1466. };
  1467. /**
  1468. * Get ids of all items or from a filtered set of items.
  1469. * @param {Object} [options] An Object with options. Available options:
  1470. * {function} [filter] filter items
  1471. * {String | function} [order] Order the items by
  1472. * a field name or custom sort function.
  1473. * @return {Array} ids
  1474. */
  1475. DataSet.prototype.getIds = function (options) {
  1476. var data = this.data,
  1477. filter = options && options.filter,
  1478. order = options && options.order,
  1479. convert = options && options.convert || this.options.convert,
  1480. i,
  1481. len,
  1482. id,
  1483. item,
  1484. items,
  1485. ids = [];
  1486. if (filter) {
  1487. // get filtered items
  1488. if (order) {
  1489. // create ordered list
  1490. items = [];
  1491. for (id in data) {
  1492. if (data.hasOwnProperty(id)) {
  1493. item = this._getItem(id, convert);
  1494. if (filter(item)) {
  1495. items.push(item);
  1496. }
  1497. }
  1498. }
  1499. this._sort(items, order);
  1500. for (i = 0, len = items.length; i < len; i++) {
  1501. ids[i] = items[i][this.fieldId];
  1502. }
  1503. }
  1504. else {
  1505. // create unordered list
  1506. for (id in data) {
  1507. if (data.hasOwnProperty(id)) {
  1508. item = this._getItem(id, convert);
  1509. if (filter(item)) {
  1510. ids.push(item[this.fieldId]);
  1511. }
  1512. }
  1513. }
  1514. }
  1515. }
  1516. else {
  1517. // get all items
  1518. if (order) {
  1519. // create an ordered list
  1520. items = [];
  1521. for (id in data) {
  1522. if (data.hasOwnProperty(id)) {
  1523. items.push(data[id]);
  1524. }
  1525. }
  1526. this._sort(items, order);
  1527. for (i = 0, len = items.length; i < len; i++) {
  1528. ids[i] = items[i][this.fieldId];
  1529. }
  1530. }
  1531. else {
  1532. // create unordered list
  1533. for (id in data) {
  1534. if (data.hasOwnProperty(id)) {
  1535. item = data[id];
  1536. ids.push(item[this.fieldId]);
  1537. }
  1538. }
  1539. }
  1540. }
  1541. return ids;
  1542. };
  1543. /**
  1544. * Execute a callback function for every item in the dataset.
  1545. * The order of the items is not determined.
  1546. * @param {function} callback
  1547. * @param {Object} [options] Available options:
  1548. * {Object.<String, String>} [convert]
  1549. * {String[]} [fields] filter fields
  1550. * {function} [filter] filter items
  1551. * {String | function} [order] Order the items by
  1552. * a field name or custom sort function.
  1553. */
  1554. DataSet.prototype.forEach = function (callback, options) {
  1555. var filter = options && options.filter,
  1556. convert = options && options.convert || this.options.convert,
  1557. data = this.data,
  1558. item,
  1559. id;
  1560. if (options && options.order) {
  1561. // execute forEach on ordered list
  1562. var items = this.get(options);
  1563. for (var i = 0, len = items.length; i < len; i++) {
  1564. item = items[i];
  1565. id = item[this.fieldId];
  1566. callback(item, id);
  1567. }
  1568. }
  1569. else {
  1570. // unordered
  1571. for (id in data) {
  1572. if (data.hasOwnProperty(id)) {
  1573. item = this._getItem(id, convert);
  1574. if (!filter || filter(item)) {
  1575. callback(item, id);
  1576. }
  1577. }
  1578. }
  1579. }
  1580. };
  1581. /**
  1582. * Map every item in the dataset.
  1583. * @param {function} callback
  1584. * @param {Object} [options] Available options:
  1585. * {Object.<String, String>} [convert]
  1586. * {String[]} [fields] filter fields
  1587. * {function} [filter] filter items
  1588. * {String | function} [order] Order the items by
  1589. * a field name or custom sort function.
  1590. * @return {Object[]} mappedItems
  1591. */
  1592. DataSet.prototype.map = function (callback, options) {
  1593. var filter = options && options.filter,
  1594. convert = options && options.convert || this.options.convert,
  1595. mappedItems = [],
  1596. data = this.data,
  1597. item;
  1598. // convert and filter items
  1599. for (var id in data) {
  1600. if (data.hasOwnProperty(id)) {
  1601. item = this._getItem(id, convert);
  1602. if (!filter || filter(item)) {
  1603. mappedItems.push(callback(item, id));
  1604. }
  1605. }
  1606. }
  1607. // order items
  1608. if (options && options.order) {
  1609. this._sort(mappedItems, options.order);
  1610. }
  1611. return mappedItems;
  1612. };
  1613. /**
  1614. * Filter the fields of an item
  1615. * @param {Object} item
  1616. * @param {String[]} fields Field names
  1617. * @return {Object} filteredItem
  1618. * @private
  1619. */
  1620. DataSet.prototype._filterFields = function (item, fields) {
  1621. var filteredItem = {};
  1622. for (var field in item) {
  1623. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1624. filteredItem[field] = item[field];
  1625. }
  1626. }
  1627. return filteredItem;
  1628. };
  1629. /**
  1630. * Sort the provided array with items
  1631. * @param {Object[]} items
  1632. * @param {String | function} order A field name or custom sort function.
  1633. * @private
  1634. */
  1635. DataSet.prototype._sort = function (items, order) {
  1636. if (util.isString(order)) {
  1637. // order by provided field name
  1638. var name = order; // field name
  1639. items.sort(function (a, b) {
  1640. var av = a[name];
  1641. var bv = b[name];
  1642. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1643. });
  1644. }
  1645. else if (typeof order === 'function') {
  1646. // order by sort function
  1647. items.sort(order);
  1648. }
  1649. // TODO: extend order by an Object {field:String, direction:String}
  1650. // where direction can be 'asc' or 'desc'
  1651. else {
  1652. throw new TypeError('Order must be a function or a string');
  1653. }
  1654. };
  1655. /**
  1656. * Remove an object by pointer or by id
  1657. * @param {String | Number | Object | Array} id Object or id, or an array with
  1658. * objects or ids to be removed
  1659. * @param {String} [senderId] Optional sender id
  1660. * @return {Array} removedIds
  1661. */
  1662. DataSet.prototype.remove = function (id, senderId) {
  1663. var removedIds = [],
  1664. i, len, removedId;
  1665. if (id instanceof Array) {
  1666. for (i = 0, len = id.length; i < len; i++) {
  1667. removedId = this._remove(id[i]);
  1668. if (removedId != null) {
  1669. removedIds.push(removedId);
  1670. }
  1671. }
  1672. }
  1673. else {
  1674. removedId = this._remove(id);
  1675. if (removedId != null) {
  1676. removedIds.push(removedId);
  1677. }
  1678. }
  1679. if (removedIds.length) {
  1680. this._trigger('remove', {items: removedIds}, senderId);
  1681. }
  1682. return removedIds;
  1683. };
  1684. /**
  1685. * Remove an item by its id
  1686. * @param {Number | String | Object} id id or item
  1687. * @returns {Number | String | null} id
  1688. * @private
  1689. */
  1690. DataSet.prototype._remove = function (id) {
  1691. if (util.isNumber(id) || util.isString(id)) {
  1692. if (this.data[id]) {
  1693. delete this.data[id];
  1694. delete this.internalIds[id];
  1695. return id;
  1696. }
  1697. }
  1698. else if (id instanceof Object) {
  1699. var itemId = id[this.fieldId];
  1700. if (itemId && this.data[itemId]) {
  1701. delete this.data[itemId];
  1702. delete this.internalIds[itemId];
  1703. return itemId;
  1704. }
  1705. }
  1706. return null;
  1707. };
  1708. /**
  1709. * Clear the data
  1710. * @param {String} [senderId] Optional sender id
  1711. * @return {Array} removedIds The ids of all removed items
  1712. */
  1713. DataSet.prototype.clear = function (senderId) {
  1714. var ids = Object.keys(this.data);
  1715. this.data = {};
  1716. this.internalIds = {};
  1717. this._trigger('remove', {items: ids}, senderId);
  1718. return ids;
  1719. };
  1720. /**
  1721. * Find the item with maximum value of a specified field
  1722. * @param {String} field
  1723. * @return {Object | null} item Item containing max value, or null if no items
  1724. */
  1725. DataSet.prototype.max = function (field) {
  1726. var data = this.data,
  1727. max = null,
  1728. maxField = null;
  1729. for (var id in data) {
  1730. if (data.hasOwnProperty(id)) {
  1731. var item = data[id];
  1732. var itemField = item[field];
  1733. if (itemField != null && (!max || itemField > maxField)) {
  1734. max = item;
  1735. maxField = itemField;
  1736. }
  1737. }
  1738. }
  1739. return max;
  1740. };
  1741. /**
  1742. * Find the item with minimum value of a specified field
  1743. * @param {String} field
  1744. * @return {Object | null} item Item containing max value, or null if no items
  1745. */
  1746. DataSet.prototype.min = function (field) {
  1747. var data = this.data,
  1748. min = null,
  1749. minField = null;
  1750. for (var id in data) {
  1751. if (data.hasOwnProperty(id)) {
  1752. var item = data[id];
  1753. var itemField = item[field];
  1754. if (itemField != null && (!min || itemField < minField)) {
  1755. min = item;
  1756. minField = itemField;
  1757. }
  1758. }
  1759. }
  1760. return min;
  1761. };
  1762. /**
  1763. * Find all distinct values of a specified field
  1764. * @param {String} field
  1765. * @return {Array} values Array containing all distinct values. If the data
  1766. * items do not contain the specified field, an array
  1767. * containing a single value undefined is returned.
  1768. * The returned array is unordered.
  1769. */
  1770. DataSet.prototype.distinct = function (field) {
  1771. var data = this.data,
  1772. values = [],
  1773. fieldType = this.options.convert[field],
  1774. count = 0;
  1775. for (var prop in data) {
  1776. if (data.hasOwnProperty(prop)) {
  1777. var item = data[prop];
  1778. var value = util.convert(item[field], fieldType);
  1779. var exists = false;
  1780. for (var i = 0; i < count; i++) {
  1781. if (values[i] == value) {
  1782. exists = true;
  1783. break;
  1784. }
  1785. }
  1786. if (!exists) {
  1787. values[count] = value;
  1788. count++;
  1789. }
  1790. }
  1791. }
  1792. return values;
  1793. };
  1794. /**
  1795. * Add a single item. Will fail when an item with the same id already exists.
  1796. * @param {Object} item
  1797. * @return {String} id
  1798. * @private
  1799. */
  1800. DataSet.prototype._addItem = function (item) {
  1801. var id = item[this.fieldId];
  1802. if (id != undefined) {
  1803. // check whether this id is already taken
  1804. if (this.data[id]) {
  1805. // item already exists
  1806. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1807. }
  1808. }
  1809. else {
  1810. // generate an id
  1811. id = util.randomUUID();
  1812. item[this.fieldId] = id;
  1813. this.internalIds[id] = item;
  1814. }
  1815. var d = {};
  1816. for (var field in item) {
  1817. if (item.hasOwnProperty(field)) {
  1818. var fieldType = this.convert[field]; // type may be undefined
  1819. d[field] = util.convert(item[field], fieldType);
  1820. }
  1821. }
  1822. this.data[id] = d;
  1823. return id;
  1824. };
  1825. /**
  1826. * Get an item. Fields can be converted to a specific type
  1827. * @param {String} id
  1828. * @param {Object.<String, String>} [convert] field types to convert
  1829. * @return {Object | null} item
  1830. * @private
  1831. */
  1832. DataSet.prototype._getItem = function (id, convert) {
  1833. var field, value;
  1834. // get the item from the dataset
  1835. var raw = this.data[id];
  1836. if (!raw) {
  1837. return null;
  1838. }
  1839. // convert the items field types
  1840. var converted = {},
  1841. fieldId = this.fieldId,
  1842. internalIds = this.internalIds;
  1843. if (convert) {
  1844. for (field in raw) {
  1845. if (raw.hasOwnProperty(field)) {
  1846. value = raw[field];
  1847. // output all fields, except internal ids
  1848. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1849. converted[field] = util.convert(value, convert[field]);
  1850. }
  1851. }
  1852. }
  1853. }
  1854. else {
  1855. // no field types specified, no converting needed
  1856. for (field in raw) {
  1857. if (raw.hasOwnProperty(field)) {
  1858. value = raw[field];
  1859. // output all fields, except internal ids
  1860. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1861. converted[field] = value;
  1862. }
  1863. }
  1864. }
  1865. }
  1866. return converted;
  1867. };
  1868. /**
  1869. * Update a single item: merge with existing item.
  1870. * Will fail when the item has no id, or when there does not exist an item
  1871. * with the same id.
  1872. * @param {Object} item
  1873. * @return {String} id
  1874. * @private
  1875. */
  1876. DataSet.prototype._updateItem = function (item) {
  1877. var id = item[this.fieldId];
  1878. if (id == undefined) {
  1879. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1880. }
  1881. var d = this.data[id];
  1882. if (!d) {
  1883. // item doesn't exist
  1884. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1885. }
  1886. // merge with current item
  1887. for (var field in item) {
  1888. if (item.hasOwnProperty(field)) {
  1889. var fieldType = this.convert[field]; // type may be undefined
  1890. d[field] = util.convert(item[field], fieldType);
  1891. }
  1892. }
  1893. return id;
  1894. };
  1895. /**
  1896. * check if an id is an internal or external id
  1897. * @param id
  1898. * @returns {boolean}
  1899. * @private
  1900. */
  1901. DataSet.prototype.isInternalId = function(id) {
  1902. return (id in this.internalIds);
  1903. };
  1904. /**
  1905. * Get an array with the column names of a Google DataTable
  1906. * @param {DataTable} dataTable
  1907. * @return {String[]} columnNames
  1908. * @private
  1909. */
  1910. DataSet.prototype._getColumnNames = function (dataTable) {
  1911. var columns = [];
  1912. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1913. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1914. }
  1915. return columns;
  1916. };
  1917. /**
  1918. * Append an item as a row to the dataTable
  1919. * @param dataTable
  1920. * @param columns
  1921. * @param item
  1922. * @private
  1923. */
  1924. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1925. var row = dataTable.addRow();
  1926. for (var col = 0, cols = columns.length; col < cols; col++) {
  1927. var field = columns[col];
  1928. dataTable.setValue(row, col, item[field]);
  1929. }
  1930. };
  1931. /**
  1932. * DataView
  1933. *
  1934. * a dataview offers a filtered view on a dataset or an other dataview.
  1935. *
  1936. * @param {DataSet | DataView} data
  1937. * @param {Object} [options] Available options: see method get
  1938. *
  1939. * @constructor DataView
  1940. */
  1941. function DataView (data, options) {
  1942. this.id = util.randomUUID();
  1943. this.data = null;
  1944. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1945. this.options = options || {};
  1946. this.fieldId = 'id'; // name of the field containing id
  1947. this.subscribers = {}; // event subscribers
  1948. var me = this;
  1949. this.listener = function () {
  1950. me._onEvent.apply(me, arguments);
  1951. };
  1952. this.setData(data);
  1953. }
  1954. // TODO: implement a function .config() to dynamically update things like configured filter
  1955. // and trigger changes accordingly
  1956. /**
  1957. * Set a data source for the view
  1958. * @param {DataSet | DataView} data
  1959. */
  1960. DataView.prototype.setData = function (data) {
  1961. var ids, dataItems, i, len;
  1962. if (this.data) {
  1963. // unsubscribe from current dataset
  1964. if (this.data.unsubscribe) {
  1965. this.data.unsubscribe('*', this.listener);
  1966. }
  1967. // trigger a remove of all items in memory
  1968. ids = [];
  1969. for (var id in this.ids) {
  1970. if (this.ids.hasOwnProperty(id)) {
  1971. ids.push(id);
  1972. }
  1973. }
  1974. this.ids = {};
  1975. this._trigger('remove', {items: ids});
  1976. }
  1977. this.data = data;
  1978. if (this.data) {
  1979. // update fieldId
  1980. this.fieldId = this.options.fieldId ||
  1981. (this.data && this.data.options && this.data.options.fieldId) ||
  1982. 'id';
  1983. // trigger an add of all added items
  1984. ids = this.data.getIds({filter: this.options && this.options.filter});
  1985. for (i = 0, len = ids.length; i < len; i++) {
  1986. id = ids[i];
  1987. this.ids[id] = true;
  1988. }
  1989. this._trigger('add', {items: ids});
  1990. // subscribe to new dataset
  1991. if (this.data.subscribe) {
  1992. this.data.subscribe('*', this.listener);
  1993. }
  1994. }
  1995. };
  1996. /**
  1997. * Get data from the data view
  1998. *
  1999. * Usage:
  2000. *
  2001. * get()
  2002. * get(options: Object)
  2003. * get(options: Object, data: Array | DataTable)
  2004. *
  2005. * get(id: Number)
  2006. * get(id: Number, options: Object)
  2007. * get(id: Number, options: Object, data: Array | DataTable)
  2008. *
  2009. * get(ids: Number[])
  2010. * get(ids: Number[], options: Object)
  2011. * get(ids: Number[], options: Object, data: Array | DataTable)
  2012. *
  2013. * Where:
  2014. *
  2015. * {Number | String} id The id of an item
  2016. * {Number[] | String{}} ids An array with ids of items
  2017. * {Object} options An Object with options. Available options:
  2018. * {String} [type] Type of data to be returned. Can
  2019. * be 'DataTable' or 'Array' (default)
  2020. * {Object.<String, String>} [convert]
  2021. * {String[]} [fields] field names to be returned
  2022. * {function} [filter] filter items
  2023. * {String | function} [order] Order the items by
  2024. * a field name or custom sort function.
  2025. * {Array | DataTable} [data] If provided, items will be appended to this
  2026. * array or table. Required in case of Google
  2027. * DataTable.
  2028. * @param args
  2029. */
  2030. DataView.prototype.get = function (args) {
  2031. var me = this;
  2032. // parse the arguments
  2033. var ids, options, data;
  2034. var firstType = util.getType(arguments[0]);
  2035. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2036. // get(id(s) [, options] [, data])
  2037. ids = arguments[0]; // can be a single id or an array with ids
  2038. options = arguments[1];
  2039. data = arguments[2];
  2040. }
  2041. else {
  2042. // get([, options] [, data])
  2043. options = arguments[0];
  2044. data = arguments[1];
  2045. }
  2046. // extend the options with the default options and provided options
  2047. var viewOptions = util.extend({}, this.options, options);
  2048. // create a combined filter method when needed
  2049. if (this.options.filter && options && options.filter) {
  2050. viewOptions.filter = function (item) {
  2051. return me.options.filter(item) && options.filter(item);
  2052. }
  2053. }
  2054. // build up the call to the linked data set
  2055. var getArguments = [];
  2056. if (ids != undefined) {
  2057. getArguments.push(ids);
  2058. }
  2059. getArguments.push(viewOptions);
  2060. getArguments.push(data);
  2061. return this.data && this.data.get.apply(this.data, getArguments);
  2062. };
  2063. /**
  2064. * Get ids of all items or from a filtered set of items.
  2065. * @param {Object} [options] An Object with options. Available options:
  2066. * {function} [filter] filter items
  2067. * {String | function} [order] Order the items by
  2068. * a field name or custom sort function.
  2069. * @return {Array} ids
  2070. */
  2071. DataView.prototype.getIds = function (options) {
  2072. var ids;
  2073. if (this.data) {
  2074. var defaultFilter = this.options.filter;
  2075. var filter;
  2076. if (options && options.filter) {
  2077. if (defaultFilter) {
  2078. filter = function (item) {
  2079. return defaultFilter(item) && options.filter(item);
  2080. }
  2081. }
  2082. else {
  2083. filter = options.filter;
  2084. }
  2085. }
  2086. else {
  2087. filter = defaultFilter;
  2088. }
  2089. ids = this.data.getIds({
  2090. filter: filter,
  2091. order: options && options.order
  2092. });
  2093. }
  2094. else {
  2095. ids = [];
  2096. }
  2097. return ids;
  2098. };
  2099. /**
  2100. * Event listener. Will propagate all events from the connected data set to
  2101. * the subscribers of the DataView, but will filter the items and only trigger
  2102. * when there are changes in the filtered data set.
  2103. * @param {String} event
  2104. * @param {Object | null} params
  2105. * @param {String} senderId
  2106. * @private
  2107. */
  2108. DataView.prototype._onEvent = function (event, params, senderId) {
  2109. var i, len, id, item,
  2110. ids = params && params.items,
  2111. data = this.data,
  2112. added = [],
  2113. updated = [],
  2114. removed = [];
  2115. if (ids && data) {
  2116. switch (event) {
  2117. case 'add':
  2118. // filter the ids of the added items
  2119. for (i = 0, len = ids.length; i < len; i++) {
  2120. id = ids[i];
  2121. item = this.get(id);
  2122. if (item) {
  2123. this.ids[id] = true;
  2124. added.push(id);
  2125. }
  2126. }
  2127. break;
  2128. case 'update':
  2129. // determine the event from the views viewpoint: an updated
  2130. // item can be added, updated, or removed from this view.
  2131. for (i = 0, len = ids.length; i < len; i++) {
  2132. id = ids[i];
  2133. item = this.get(id);
  2134. if (item) {
  2135. if (this.ids[id]) {
  2136. updated.push(id);
  2137. }
  2138. else {
  2139. this.ids[id] = true;
  2140. added.push(id);
  2141. }
  2142. }
  2143. else {
  2144. if (this.ids[id]) {
  2145. delete this.ids[id];
  2146. removed.push(id);
  2147. }
  2148. else {
  2149. // nothing interesting for me :-(
  2150. }
  2151. }
  2152. }
  2153. break;
  2154. case 'remove':
  2155. // filter the ids of the removed items
  2156. for (i = 0, len = ids.length; i < len; i++) {
  2157. id = ids[i];
  2158. if (this.ids[id]) {
  2159. delete this.ids[id];
  2160. removed.push(id);
  2161. }
  2162. }
  2163. break;
  2164. }
  2165. if (added.length) {
  2166. this._trigger('add', {items: added}, senderId);
  2167. }
  2168. if (updated.length) {
  2169. this._trigger('update', {items: updated}, senderId);
  2170. }
  2171. if (removed.length) {
  2172. this._trigger('remove', {items: removed}, senderId);
  2173. }
  2174. }
  2175. };
  2176. // copy subscription functionality from DataSet
  2177. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2178. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2179. DataView.prototype._trigger = DataSet.prototype._trigger;
  2180. /**
  2181. * @constructor TimeStep
  2182. * The class TimeStep is an iterator for dates. You provide a start date and an
  2183. * end date. The class itself determines the best scale (step size) based on the
  2184. * provided start Date, end Date, and minimumStep.
  2185. *
  2186. * If minimumStep is provided, the step size is chosen as close as possible
  2187. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2188. * provided, the scale is set to 1 DAY.
  2189. * The minimumStep should correspond with the onscreen size of about 6 characters
  2190. *
  2191. * Alternatively, you can set a scale by hand.
  2192. * After creation, you can initialize the class by executing first(). Then you
  2193. * can iterate from the start date to the end date via next(). You can check if
  2194. * the end date is reached with the function hasNext(). After each step, you can
  2195. * retrieve the current date via getCurrent().
  2196. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2197. * days, to years.
  2198. *
  2199. * Version: 1.2
  2200. *
  2201. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2202. * or new Date(2010, 9, 21, 23, 45, 00)
  2203. * @param {Date} [end] The end date
  2204. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2205. */
  2206. TimeStep = function(start, end, minimumStep) {
  2207. // variables
  2208. this.current = new Date();
  2209. this._start = new Date();
  2210. this._end = new Date();
  2211. this.autoScale = true;
  2212. this.scale = TimeStep.SCALE.DAY;
  2213. this.step = 1;
  2214. // initialize the range
  2215. this.setRange(start, end, minimumStep);
  2216. };
  2217. /// enum scale
  2218. TimeStep.SCALE = {
  2219. MILLISECOND: 1,
  2220. SECOND: 2,
  2221. MINUTE: 3,
  2222. HOUR: 4,
  2223. DAY: 5,
  2224. WEEKDAY: 6,
  2225. MONTH: 7,
  2226. YEAR: 8
  2227. };
  2228. /**
  2229. * Set a new range
  2230. * If minimumStep is provided, the step size is chosen as close as possible
  2231. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2232. * provided, the scale is set to 1 DAY.
  2233. * The minimumStep should correspond with the onscreen size of about 6 characters
  2234. * @param {Date} [start] The start date and time.
  2235. * @param {Date} [end] The end date and time.
  2236. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2237. */
  2238. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2239. if (!(start instanceof Date) || !(end instanceof Date)) {
  2240. throw "No legal start or end date in method setRange";
  2241. }
  2242. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2243. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2244. if (this.autoScale) {
  2245. this.setMinimumStep(minimumStep);
  2246. }
  2247. };
  2248. /**
  2249. * Set the range iterator to the start date.
  2250. */
  2251. TimeStep.prototype.first = function() {
  2252. this.current = new Date(this._start.valueOf());
  2253. this.roundToMinor();
  2254. };
  2255. /**
  2256. * Round the current date to the first minor date value
  2257. * This must be executed once when the current date is set to start Date
  2258. */
  2259. TimeStep.prototype.roundToMinor = function() {
  2260. // round to floor
  2261. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2262. //noinspection FallthroughInSwitchStatementJS
  2263. switch (this.scale) {
  2264. case TimeStep.SCALE.YEAR:
  2265. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2266. this.current.setMonth(0);
  2267. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2268. case TimeStep.SCALE.DAY: // intentional fall through
  2269. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2270. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2271. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2272. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2273. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2274. }
  2275. if (this.step != 1) {
  2276. // round down to the first minor value that is a multiple of the current step size
  2277. switch (this.scale) {
  2278. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2279. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2280. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2281. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2282. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2283. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2284. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2285. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2286. default: break;
  2287. }
  2288. }
  2289. };
  2290. /**
  2291. * Check if the there is a next step
  2292. * @return {boolean} true if the current date has not passed the end date
  2293. */
  2294. TimeStep.prototype.hasNext = function () {
  2295. return (this.current.valueOf() <= this._end.valueOf());
  2296. };
  2297. /**
  2298. * Do the next step
  2299. */
  2300. TimeStep.prototype.next = function() {
  2301. var prev = this.current.valueOf();
  2302. // Two cases, needed to prevent issues with switching daylight savings
  2303. // (end of March and end of October)
  2304. if (this.current.getMonth() < 6) {
  2305. switch (this.scale) {
  2306. case TimeStep.SCALE.MILLISECOND:
  2307. this.current = new Date(this.current.valueOf() + this.step); break;
  2308. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2309. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2310. case TimeStep.SCALE.HOUR:
  2311. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2312. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2313. var h = this.current.getHours();
  2314. this.current.setHours(h - (h % this.step));
  2315. break;
  2316. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2317. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2318. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2319. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2320. default: break;
  2321. }
  2322. }
  2323. else {
  2324. switch (this.scale) {
  2325. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2326. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2327. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2328. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2329. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2330. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2331. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2332. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2333. default: break;
  2334. }
  2335. }
  2336. if (this.step != 1) {
  2337. // round down to the correct major value
  2338. switch (this.scale) {
  2339. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2340. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2341. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2342. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2343. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2344. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2345. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2346. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2347. default: break;
  2348. }
  2349. }
  2350. // safety mechanism: if current time is still unchanged, move to the end
  2351. if (this.current.valueOf() == prev) {
  2352. this.current = new Date(this._end.valueOf());
  2353. }
  2354. };
  2355. /**
  2356. * Get the current datetime
  2357. * @return {Date} current The current date
  2358. */
  2359. TimeStep.prototype.getCurrent = function() {
  2360. return this.current;
  2361. };
  2362. /**
  2363. * Set a custom scale. Autoscaling will be disabled.
  2364. * For example setScale(SCALE.MINUTES, 5) will result
  2365. * in minor steps of 5 minutes, and major steps of an hour.
  2366. *
  2367. * @param {TimeStep.SCALE} newScale
  2368. * A scale. Choose from SCALE.MILLISECOND,
  2369. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2370. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2371. * SCALE.YEAR.
  2372. * @param {Number} newStep A step size, by default 1. Choose for
  2373. * example 1, 2, 5, or 10.
  2374. */
  2375. TimeStep.prototype.setScale = function(newScale, newStep) {
  2376. this.scale = newScale;
  2377. if (newStep > 0) {
  2378. this.step = newStep;
  2379. }
  2380. this.autoScale = false;
  2381. };
  2382. /**
  2383. * Enable or disable autoscaling
  2384. * @param {boolean} enable If true, autoascaling is set true
  2385. */
  2386. TimeStep.prototype.setAutoScale = function (enable) {
  2387. this.autoScale = enable;
  2388. };
  2389. /**
  2390. * Automatically determine the scale that bests fits the provided minimum step
  2391. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2392. */
  2393. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2394. if (minimumStep == undefined) {
  2395. return;
  2396. }
  2397. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2398. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2399. var stepDay = (1000 * 60 * 60 * 24);
  2400. var stepHour = (1000 * 60 * 60);
  2401. var stepMinute = (1000 * 60);
  2402. var stepSecond = (1000);
  2403. var stepMillisecond= (1);
  2404. // find the smallest step that is larger than the provided minimumStep
  2405. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2406. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2407. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2408. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2409. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2410. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2411. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2412. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2413. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2414. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2415. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2416. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2417. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2418. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2419. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2420. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2421. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2422. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2423. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2424. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2425. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2426. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2427. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2428. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2429. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2430. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2431. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2432. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2433. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2434. };
  2435. /**
  2436. * Snap a date to a rounded value. The snap intervals are dependent on the
  2437. * current scale and step.
  2438. * @param {Date} date the date to be snapped
  2439. */
  2440. TimeStep.prototype.snap = function(date) {
  2441. if (this.scale == TimeStep.SCALE.YEAR) {
  2442. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  2443. date.setFullYear(Math.round(year / this.step) * this.step);
  2444. date.setMonth(0);
  2445. date.setDate(0);
  2446. date.setHours(0);
  2447. date.setMinutes(0);
  2448. date.setSeconds(0);
  2449. date.setMilliseconds(0);
  2450. }
  2451. else if (this.scale == TimeStep.SCALE.MONTH) {
  2452. if (date.getDate() > 15) {
  2453. date.setDate(1);
  2454. date.setMonth(date.getMonth() + 1);
  2455. // important: first set Date to 1, after that change the month.
  2456. }
  2457. else {
  2458. date.setDate(1);
  2459. }
  2460. date.setHours(0);
  2461. date.setMinutes(0);
  2462. date.setSeconds(0);
  2463. date.setMilliseconds(0);
  2464. }
  2465. else if (this.scale == TimeStep.SCALE.DAY ||
  2466. this.scale == TimeStep.SCALE.WEEKDAY) {
  2467. //noinspection FallthroughInSwitchStatementJS
  2468. switch (this.step) {
  2469. case 5:
  2470. case 2:
  2471. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  2472. default:
  2473. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  2474. }
  2475. date.setMinutes(0);
  2476. date.setSeconds(0);
  2477. date.setMilliseconds(0);
  2478. }
  2479. else if (this.scale == TimeStep.SCALE.HOUR) {
  2480. switch (this.step) {
  2481. case 4:
  2482. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  2483. default:
  2484. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  2485. }
  2486. date.setSeconds(0);
  2487. date.setMilliseconds(0);
  2488. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2489. //noinspection FallthroughInSwitchStatementJS
  2490. switch (this.step) {
  2491. case 15:
  2492. case 10:
  2493. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  2494. date.setSeconds(0);
  2495. break;
  2496. case 5:
  2497. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  2498. default:
  2499. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  2500. }
  2501. date.setMilliseconds(0);
  2502. }
  2503. else if (this.scale == TimeStep.SCALE.SECOND) {
  2504. //noinspection FallthroughInSwitchStatementJS
  2505. switch (this.step) {
  2506. case 15:
  2507. case 10:
  2508. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  2509. date.setMilliseconds(0);
  2510. break;
  2511. case 5:
  2512. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  2513. default:
  2514. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  2515. }
  2516. }
  2517. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2518. var step = this.step > 5 ? this.step / 2 : 1;
  2519. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  2520. }
  2521. };
  2522. /**
  2523. * Check if the current value is a major value (for example when the step
  2524. * is DAY, a major value is each first day of the MONTH)
  2525. * @return {boolean} true if current date is major, else false.
  2526. */
  2527. TimeStep.prototype.isMajor = function() {
  2528. switch (this.scale) {
  2529. case TimeStep.SCALE.MILLISECOND:
  2530. return (this.current.getMilliseconds() == 0);
  2531. case TimeStep.SCALE.SECOND:
  2532. return (this.current.getSeconds() == 0);
  2533. case TimeStep.SCALE.MINUTE:
  2534. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2535. // Note: this is no bug. Major label is equal for both minute and hour scale
  2536. case TimeStep.SCALE.HOUR:
  2537. return (this.current.getHours() == 0);
  2538. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2539. case TimeStep.SCALE.DAY:
  2540. return (this.current.getDate() == 1);
  2541. case TimeStep.SCALE.MONTH:
  2542. return (this.current.getMonth() == 0);
  2543. case TimeStep.SCALE.YEAR:
  2544. return false;
  2545. default:
  2546. return false;
  2547. }
  2548. };
  2549. /**
  2550. * Returns formatted text for the minor axislabel, depending on the current
  2551. * date and the scale. For example when scale is MINUTE, the current time is
  2552. * formatted as "hh:mm".
  2553. * @param {Date} [date] custom date. if not provided, current date is taken
  2554. */
  2555. TimeStep.prototype.getLabelMinor = function(date) {
  2556. if (date == undefined) {
  2557. date = this.current;
  2558. }
  2559. switch (this.scale) {
  2560. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2561. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2562. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2563. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2564. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2565. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2566. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2567. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2568. default: return '';
  2569. }
  2570. };
  2571. /**
  2572. * Returns formatted text for the major axis label, depending on the current
  2573. * date and the scale. For example when scale is MINUTE, the major scale is
  2574. * hours, and the hour will be formatted as "hh".
  2575. * @param {Date} [date] custom date. if not provided, current date is taken
  2576. */
  2577. TimeStep.prototype.getLabelMajor = function(date) {
  2578. if (date == undefined) {
  2579. date = this.current;
  2580. }
  2581. //noinspection FallthroughInSwitchStatementJS
  2582. switch (this.scale) {
  2583. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2584. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2585. case TimeStep.SCALE.MINUTE:
  2586. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2587. case TimeStep.SCALE.WEEKDAY:
  2588. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2589. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2590. case TimeStep.SCALE.YEAR: return '';
  2591. default: return '';
  2592. }
  2593. };
  2594. /**
  2595. * @constructor Stack
  2596. * Stacks items on top of each other.
  2597. * @param {ItemSet} parent
  2598. * @param {Object} [options]
  2599. */
  2600. function Stack (parent, options) {
  2601. this.parent = parent;
  2602. this.options = options || {};
  2603. this.defaultOptions = {
  2604. order: function (a, b) {
  2605. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2606. // Order: ranges over non-ranges, ranged ordered by width, and
  2607. // lastly ordered by start.
  2608. if (a instanceof ItemRange) {
  2609. if (b instanceof ItemRange) {
  2610. var aInt = (a.data.end - a.data.start);
  2611. var bInt = (b.data.end - b.data.start);
  2612. return (aInt - bInt) || (a.data.start - b.data.start);
  2613. }
  2614. else {
  2615. return -1;
  2616. }
  2617. }
  2618. else {
  2619. if (b instanceof ItemRange) {
  2620. return 1;
  2621. }
  2622. else {
  2623. return (a.data.start - b.data.start);
  2624. }
  2625. }
  2626. },
  2627. margin: {
  2628. item: 10
  2629. }
  2630. };
  2631. this.ordered = []; // ordered items
  2632. }
  2633. /**
  2634. * Set options for the stack
  2635. * @param {Object} options Available options:
  2636. * {ItemSet} parent
  2637. * {Number} margin
  2638. * {function} order Stacking order
  2639. */
  2640. Stack.prototype.setOptions = function setOptions (options) {
  2641. util.extend(this.options, options);
  2642. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2643. };
  2644. /**
  2645. * Stack the items such that they don't overlap. The items will have a minimal
  2646. * distance equal to options.margin.item.
  2647. */
  2648. Stack.prototype.update = function update() {
  2649. this._order();
  2650. this._stack();
  2651. };
  2652. /**
  2653. * Order the items. The items are ordered by width first, and by left position
  2654. * second.
  2655. * If a custom order function has been provided via the options, then this will
  2656. * be used.
  2657. * @private
  2658. */
  2659. Stack.prototype._order = function _order () {
  2660. var items = this.parent.items;
  2661. if (!items) {
  2662. throw new Error('Cannot stack items: parent does not contain items');
  2663. }
  2664. // TODO: store the sorted items, to have less work later on
  2665. var ordered = [];
  2666. var index = 0;
  2667. // items is a map (no array)
  2668. util.forEach(items, function (item) {
  2669. if (item.visible) {
  2670. ordered[index] = item;
  2671. index++;
  2672. }
  2673. });
  2674. //if a customer stack order function exists, use it.
  2675. var order = this.options.order || this.defaultOptions.order;
  2676. if (!(typeof order === 'function')) {
  2677. throw new Error('Option order must be a function');
  2678. }
  2679. ordered.sort(order);
  2680. this.ordered = ordered;
  2681. };
  2682. /**
  2683. * Adjust vertical positions of the events such that they don't overlap each
  2684. * other.
  2685. * @private
  2686. */
  2687. Stack.prototype._stack = function _stack () {
  2688. var i,
  2689. iMax,
  2690. ordered = this.ordered,
  2691. options = this.options,
  2692. orientation = options.orientation || this.defaultOptions.orientation,
  2693. axisOnTop = (orientation == 'top'),
  2694. margin;
  2695. if (options.margin && options.margin.item !== undefined) {
  2696. margin = options.margin.item;
  2697. }
  2698. else {
  2699. margin = this.defaultOptions.margin.item
  2700. }
  2701. // calculate new, non-overlapping positions
  2702. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2703. var item = ordered[i];
  2704. var collidingItem = null;
  2705. do {
  2706. // TODO: optimize checking for overlap. when there is a gap without items,
  2707. // you only need to check for items from the next item on, not from zero
  2708. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2709. if (collidingItem != null) {
  2710. // There is a collision. Reposition the event above the colliding element
  2711. if (axisOnTop) {
  2712. item.top = collidingItem.top + collidingItem.height + margin;
  2713. }
  2714. else {
  2715. item.top = collidingItem.top - item.height - margin;
  2716. }
  2717. }
  2718. } while (collidingItem);
  2719. }
  2720. };
  2721. /**
  2722. * Check if the destiny position of given item overlaps with any
  2723. * of the other items from index itemStart to itemEnd.
  2724. * @param {Array} items Array with items
  2725. * @param {int} itemIndex Number of the item to be checked for overlap
  2726. * @param {int} itemStart First item to be checked.
  2727. * @param {int} itemEnd Last item to be checked.
  2728. * @return {Object | null} colliding item, or undefined when no collisions
  2729. * @param {Number} margin A minimum required margin.
  2730. * If margin is provided, the two items will be
  2731. * marked colliding when they overlap or
  2732. * when the margin between the two is smaller than
  2733. * the requested margin.
  2734. */
  2735. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2736. itemStart, itemEnd, margin) {
  2737. var collision = this.collision;
  2738. // we loop from end to start, as we suppose that the chance of a
  2739. // collision is larger for items at the end, so check these first.
  2740. var a = items[itemIndex];
  2741. for (var i = itemEnd; i >= itemStart; i--) {
  2742. var b = items[i];
  2743. if (collision(a, b, margin)) {
  2744. if (i != itemIndex) {
  2745. return b;
  2746. }
  2747. }
  2748. }
  2749. return null;
  2750. };
  2751. /**
  2752. * Test if the two provided items collide
  2753. * The items must have parameters left, width, top, and height.
  2754. * @param {Component} a The first item
  2755. * @param {Component} b The second item
  2756. * @param {Number} margin A minimum required margin.
  2757. * If margin is provided, the two items will be
  2758. * marked colliding when they overlap or
  2759. * when the margin between the two is smaller than
  2760. * the requested margin.
  2761. * @return {boolean} true if a and b collide, else false
  2762. */
  2763. Stack.prototype.collision = function collision (a, b, margin) {
  2764. return ((a.left - margin) < (b.left + b.getWidth()) &&
  2765. (a.left + a.getWidth() + margin) > b.left &&
  2766. (a.top - margin) < (b.top + b.height) &&
  2767. (a.top + a.height + margin) > b.top);
  2768. };
  2769. /**
  2770. * @constructor Range
  2771. * A Range controls a numeric range with a start and end value.
  2772. * The Range adjusts the range based on mouse events or programmatic changes,
  2773. * and triggers events when the range is changing or has been changed.
  2774. * @param {Object} [options] See description at Range.setOptions
  2775. * @extends Controller
  2776. */
  2777. function Range(options) {
  2778. this.id = util.randomUUID();
  2779. this.start = null; // Number
  2780. this.end = null; // Number
  2781. this.options = options || {};
  2782. this.setOptions(options);
  2783. }
  2784. /**
  2785. * Set options for the range controller
  2786. * @param {Object} options Available options:
  2787. * {Number} min Minimum value for start
  2788. * {Number} max Maximum value for end
  2789. * {Number} zoomMin Set a minimum value for
  2790. * (end - start).
  2791. * {Number} zoomMax Set a maximum value for
  2792. * (end - start).
  2793. */
  2794. Range.prototype.setOptions = function (options) {
  2795. util.extend(this.options, options);
  2796. // re-apply range with new limitations
  2797. if (this.start !== null && this.end !== null) {
  2798. this.setRange(this.start, this.end);
  2799. }
  2800. };
  2801. /**
  2802. * Test whether direction has a valid value
  2803. * @param {String} direction 'horizontal' or 'vertical'
  2804. */
  2805. function validateDirection (direction) {
  2806. if (direction != 'horizontal' && direction != 'vertical') {
  2807. throw new TypeError('Unknown direction "' + direction + '". ' +
  2808. 'Choose "horizontal" or "vertical".');
  2809. }
  2810. }
  2811. /**
  2812. * Add listeners for mouse and touch events to the component
  2813. * @param {Component} component
  2814. * @param {String} event Available events: 'move', 'zoom'
  2815. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2816. */
  2817. Range.prototype.subscribe = function (component, event, direction) {
  2818. var me = this;
  2819. if (event == 'move') {
  2820. // drag start listener
  2821. component.on('dragstart', function (event) {
  2822. me._onDragStart(event, component);
  2823. });
  2824. // drag listener
  2825. component.on('drag', function (event) {
  2826. me._onDrag(event, component, direction);
  2827. });
  2828. // drag end listener
  2829. component.on('dragend', function (event) {
  2830. me._onDragEnd(event, component);
  2831. });
  2832. }
  2833. else if (event == 'zoom') {
  2834. // mouse wheel
  2835. function mousewheel (event) {
  2836. me._onMouseWheel(event, component, direction);
  2837. }
  2838. component.on('mousewheel', mousewheel);
  2839. component.on('DOMMouseScroll', mousewheel); // For FF
  2840. // pinch
  2841. component.on('touch', function (event) {
  2842. me._onTouch();
  2843. });
  2844. component.on('pinch', function (event) {
  2845. me._onPinch(event, component, direction);
  2846. });
  2847. }
  2848. else {
  2849. throw new TypeError('Unknown event "' + event + '". ' +
  2850. 'Choose "move" or "zoom".');
  2851. }
  2852. };
  2853. /**
  2854. * Add event listener
  2855. * @param {String} event Name of the event.
  2856. * Available events: 'rangechange', 'rangechanged'
  2857. * @param {function} callback Callback function, invoked as callback({start: Date, end: Date})
  2858. */
  2859. Range.prototype.on = function on (event, callback) {
  2860. var available = ['rangechange', 'rangechanged'];
  2861. if (available.indexOf(event) == -1) {
  2862. throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
  2863. }
  2864. events.addListener(this, event, callback);
  2865. };
  2866. /**
  2867. * Remove an event listener
  2868. * @param {String} event name of the event
  2869. * @param {function} callback callback handler
  2870. */
  2871. Range.prototype.off = function off (event, callback) {
  2872. events.removeListener(this, event, callback);
  2873. };
  2874. /**
  2875. * Trigger an event
  2876. * @param {String} event name of the event, available events: 'rangechange',
  2877. * 'rangechanged'
  2878. * @private
  2879. */
  2880. Range.prototype._trigger = function (event) {
  2881. events.trigger(this, event, {
  2882. start: this.start,
  2883. end: this.end
  2884. });
  2885. };
  2886. /**
  2887. * Set a new start and end range
  2888. * @param {Number} [start]
  2889. * @param {Number} [end]
  2890. */
  2891. Range.prototype.setRange = function(start, end) {
  2892. var changed = this._applyRange(start, end);
  2893. if (changed) {
  2894. this._trigger('rangechange');
  2895. this._trigger('rangechanged');
  2896. }
  2897. };
  2898. /**
  2899. * Set a new start and end range. This method is the same as setRange, but
  2900. * does not trigger a range change and range changed event, and it returns
  2901. * true when the range is changed
  2902. * @param {Number} [start]
  2903. * @param {Number} [end]
  2904. * @return {Boolean} changed
  2905. * @private
  2906. */
  2907. Range.prototype._applyRange = function(start, end) {
  2908. var newStart = (start != null) ? util.convert(start, 'Number') : this.start,
  2909. newEnd = (end != null) ? util.convert(end, 'Number') : this.end,
  2910. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2911. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2912. diff;
  2913. // check for valid number
  2914. if (isNaN(newStart) || newStart === null) {
  2915. throw new Error('Invalid start "' + start + '"');
  2916. }
  2917. if (isNaN(newEnd) || newEnd === null) {
  2918. throw new Error('Invalid end "' + end + '"');
  2919. }
  2920. // prevent start < end
  2921. if (newEnd < newStart) {
  2922. newEnd = newStart;
  2923. }
  2924. // prevent start < min
  2925. if (min !== null) {
  2926. if (newStart < min) {
  2927. diff = (min - newStart);
  2928. newStart += diff;
  2929. newEnd += diff;
  2930. // prevent end > max
  2931. if (max != null) {
  2932. if (newEnd > max) {
  2933. newEnd = max;
  2934. }
  2935. }
  2936. }
  2937. }
  2938. // prevent end > max
  2939. if (max !== null) {
  2940. if (newEnd > max) {
  2941. diff = (newEnd - max);
  2942. newStart -= diff;
  2943. newEnd -= diff;
  2944. // prevent start < min
  2945. if (min != null) {
  2946. if (newStart < min) {
  2947. newStart = min;
  2948. }
  2949. }
  2950. }
  2951. }
  2952. // prevent (end-start) < zoomMin
  2953. if (this.options.zoomMin !== null) {
  2954. var zoomMin = parseFloat(this.options.zoomMin);
  2955. if (zoomMin < 0) {
  2956. zoomMin = 0;
  2957. }
  2958. if ((newEnd - newStart) < zoomMin) {
  2959. if ((this.end - this.start) === zoomMin) {
  2960. // ignore this action, we are already zoomed to the minimum
  2961. newStart = this.start;
  2962. newEnd = this.end;
  2963. }
  2964. else {
  2965. // zoom to the minimum
  2966. diff = (zoomMin - (newEnd - newStart));
  2967. newStart -= diff / 2;
  2968. newEnd += diff / 2;
  2969. }
  2970. }
  2971. }
  2972. // prevent (end-start) > zoomMax
  2973. if (this.options.zoomMax !== null) {
  2974. var zoomMax = parseFloat(this.options.zoomMax);
  2975. if (zoomMax < 0) {
  2976. zoomMax = 0;
  2977. }
  2978. if ((newEnd - newStart) > zoomMax) {
  2979. if ((this.end - this.start) === zoomMax) {
  2980. // ignore this action, we are already zoomed to the maximum
  2981. newStart = this.start;
  2982. newEnd = this.end;
  2983. }
  2984. else {
  2985. // zoom to the maximum
  2986. diff = ((newEnd - newStart) - zoomMax);
  2987. newStart += diff / 2;
  2988. newEnd -= diff / 2;
  2989. }
  2990. }
  2991. }
  2992. var changed = (this.start != newStart || this.end != newEnd);
  2993. this.start = newStart;
  2994. this.end = newEnd;
  2995. return changed;
  2996. };
  2997. /**
  2998. * Retrieve the current range.
  2999. * @return {Object} An object with start and end properties
  3000. */
  3001. Range.prototype.getRange = function() {
  3002. return {
  3003. start: this.start,
  3004. end: this.end
  3005. };
  3006. };
  3007. /**
  3008. * Calculate the conversion offset and scale for current range, based on
  3009. * the provided width
  3010. * @param {Number} width
  3011. * @returns {{offset: number, scale: number}} conversion
  3012. */
  3013. Range.prototype.conversion = function (width) {
  3014. return Range.conversion(this.start, this.end, width);
  3015. };
  3016. /**
  3017. * Static method to calculate the conversion offset and scale for a range,
  3018. * based on the provided start, end, and width
  3019. * @param {Number} start
  3020. * @param {Number} end
  3021. * @param {Number} width
  3022. * @returns {{offset: number, scale: number}} conversion
  3023. */
  3024. Range.conversion = function (start, end, width) {
  3025. if (width != 0 && (end - start != 0)) {
  3026. return {
  3027. offset: start,
  3028. scale: width / (end - start)
  3029. }
  3030. }
  3031. else {
  3032. return {
  3033. offset: 0,
  3034. scale: 1
  3035. };
  3036. }
  3037. };
  3038. // global (private) object to store drag params
  3039. var touchParams = {};
  3040. /**
  3041. * Start dragging horizontally or vertically
  3042. * @param {Event} event
  3043. * @param {Object} component
  3044. * @private
  3045. */
  3046. Range.prototype._onDragStart = function(event, component) {
  3047. // refuse to drag when we where pinching to prevent the timeline make a jump
  3048. // when releasing the fingers in opposite order from the touch screen
  3049. if (touchParams.pinching) return;
  3050. touchParams.start = this.start;
  3051. touchParams.end = this.end;
  3052. var frame = component.frame;
  3053. if (frame) {
  3054. frame.style.cursor = 'move';
  3055. }
  3056. };
  3057. /**
  3058. * Perform dragging operating.
  3059. * @param {Event} event
  3060. * @param {Component} component
  3061. * @param {String} direction 'horizontal' or 'vertical'
  3062. * @private
  3063. */
  3064. Range.prototype._onDrag = function (event, component, direction) {
  3065. validateDirection(direction);
  3066. // refuse to drag when we where pinching to prevent the timeline make a jump
  3067. // when releasing the fingers in opposite order from the touch screen
  3068. if (touchParams.pinching) return;
  3069. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3070. interval = (touchParams.end - touchParams.start),
  3071. width = (direction == 'horizontal') ? component.width : component.height,
  3072. diffRange = -delta / width * interval;
  3073. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3074. // fire a rangechange event
  3075. this._trigger('rangechange');
  3076. };
  3077. /**
  3078. * Stop dragging operating.
  3079. * @param {event} event
  3080. * @param {Component} component
  3081. * @private
  3082. */
  3083. Range.prototype._onDragEnd = function (event, component) {
  3084. // refuse to drag when we where pinching to prevent the timeline make a jump
  3085. // when releasing the fingers in opposite order from the touch screen
  3086. if (touchParams.pinching) return;
  3087. if (component.frame) {
  3088. component.frame.style.cursor = 'auto';
  3089. }
  3090. // fire a rangechanged event
  3091. this._trigger('rangechanged');
  3092. };
  3093. /**
  3094. * Event handler for mouse wheel event, used to zoom
  3095. * Code from http://adomas.org/javascript-mouse-wheel/
  3096. * @param {Event} event
  3097. * @param {Component} component
  3098. * @param {String} direction 'horizontal' or 'vertical'
  3099. * @private
  3100. */
  3101. Range.prototype._onMouseWheel = function(event, component, direction) {
  3102. validateDirection(direction);
  3103. // retrieve delta
  3104. var delta = 0;
  3105. if (event.wheelDelta) { /* IE/Opera. */
  3106. delta = event.wheelDelta / 120;
  3107. } else if (event.detail) { /* Mozilla case. */
  3108. // In Mozilla, sign of delta is different than in IE.
  3109. // Also, delta is multiple of 3.
  3110. delta = -event.detail / 3;
  3111. }
  3112. // If delta is nonzero, handle it.
  3113. // Basically, delta is now positive if wheel was scrolled up,
  3114. // and negative, if wheel was scrolled down.
  3115. if (delta) {
  3116. // perform the zoom action. Delta is normally 1 or -1
  3117. // adjust a negative delta such that zooming in with delta 0.1
  3118. // equals zooming out with a delta -0.1
  3119. var scale;
  3120. if (delta < 0) {
  3121. scale = 1 - (delta / 5);
  3122. }
  3123. else {
  3124. scale = 1 / (1 + (delta / 5)) ;
  3125. }
  3126. // calculate center, the date to zoom around
  3127. var gesture = util.fakeGesture(this, event),
  3128. pointer = getPointer(gesture.touches[0], component.frame),
  3129. pointerDate = this._pointerToDate(component, direction, pointer);
  3130. this.zoom(scale, pointerDate);
  3131. }
  3132. // Prevent default actions caused by mouse wheel
  3133. // (else the page and timeline both zoom and scroll)
  3134. util.preventDefault(event);
  3135. };
  3136. /**
  3137. * On start of a touch gesture, initialize scale to 1
  3138. * @private
  3139. */
  3140. Range.prototype._onTouch = function () {
  3141. touchParams.start = this.start;
  3142. touchParams.end = this.end;
  3143. touchParams.pinching = false;
  3144. touchParams.center = null;
  3145. };
  3146. /**
  3147. * Handle pinch event
  3148. * @param {Event} event
  3149. * @param {Component} component
  3150. * @param {String} direction 'horizontal' or 'vertical'
  3151. * @private
  3152. */
  3153. Range.prototype._onPinch = function (event, component, direction) {
  3154. touchParams.pinching = true;
  3155. if (event.gesture.touches.length > 1) {
  3156. if (!touchParams.center) {
  3157. touchParams.center = getPointer(event.gesture.center, component.frame);
  3158. }
  3159. var scale = 1 / event.gesture.scale,
  3160. initDate = this._pointerToDate(component, direction, touchParams.center),
  3161. center = getPointer(event.gesture.center, component.frame),
  3162. date = this._pointerToDate(component, direction, center),
  3163. delta = date - initDate; // TODO: utilize delta
  3164. // calculate new start and end
  3165. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3166. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3167. // apply new range
  3168. this.setRange(newStart, newEnd);
  3169. }
  3170. };
  3171. /**
  3172. * Helper function to calculate the center date for zooming
  3173. * @param {Component} component
  3174. * @param {{x: Number, y: Number}} pointer
  3175. * @param {String} direction 'horizontal' or 'vertical'
  3176. * @return {number} date
  3177. * @private
  3178. */
  3179. Range.prototype._pointerToDate = function (component, direction, pointer) {
  3180. var conversion;
  3181. if (direction == 'horizontal') {
  3182. var width = component.width;
  3183. conversion = this.conversion(width);
  3184. return pointer.x / conversion.scale + conversion.offset;
  3185. }
  3186. else {
  3187. var height = component.height;
  3188. conversion = this.conversion(height);
  3189. return pointer.y / conversion.scale + conversion.offset;
  3190. }
  3191. };
  3192. /**
  3193. * Get the pointer location relative to the location of the dom element
  3194. * @param {{pageX: Number, pageY: Number}} touch
  3195. * @param {Element} element HTML DOM element
  3196. * @return {{x: Number, y: Number}} pointer
  3197. * @private
  3198. */
  3199. function getPointer (touch, element) {
  3200. return {
  3201. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3202. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3203. };
  3204. }
  3205. /**
  3206. * Zoom the range the given scale in or out. Start and end date will
  3207. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3208. * date around which to zoom.
  3209. * For example, try scale = 0.9 or 1.1
  3210. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3211. * values below 1 will zoom in.
  3212. * @param {Number} [center] Value representing a date around which will
  3213. * be zoomed.
  3214. */
  3215. Range.prototype.zoom = function(scale, center) {
  3216. // if centerDate is not provided, take it half between start Date and end Date
  3217. if (center == null) {
  3218. center = (this.start + this.end) / 2;
  3219. }
  3220. // calculate new start and end
  3221. var newStart = center + (this.start - center) * scale;
  3222. var newEnd = center + (this.end - center) * scale;
  3223. this.setRange(newStart, newEnd);
  3224. };
  3225. /**
  3226. * Move the range with a given delta to the left or right. Start and end
  3227. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3228. * @param {Number} delta Moving amount. Positive value will move right,
  3229. * negative value will move left
  3230. */
  3231. Range.prototype.move = function(delta) {
  3232. // zoom start Date and end Date relative to the centerDate
  3233. var diff = (this.end - this.start);
  3234. // apply new values
  3235. var newStart = this.start + diff * delta;
  3236. var newEnd = this.end + diff * delta;
  3237. // TODO: reckon with min and max range
  3238. this.start = newStart;
  3239. this.end = newEnd;
  3240. };
  3241. /**
  3242. * Move the range to a new center point
  3243. * @param {Number} moveTo New center point of the range
  3244. */
  3245. Range.prototype.moveTo = function(moveTo) {
  3246. var center = (this.start + this.end) / 2;
  3247. var diff = center - moveTo;
  3248. // calculate new start and end
  3249. var newStart = this.start - diff;
  3250. var newEnd = this.end - diff;
  3251. this.setRange(newStart, newEnd);
  3252. };
  3253. /**
  3254. * @constructor Controller
  3255. *
  3256. * A Controller controls the reflows and repaints of all visual components
  3257. */
  3258. function Controller () {
  3259. this.id = util.randomUUID();
  3260. this.components = {};
  3261. this.repaintTimer = undefined;
  3262. this.reflowTimer = undefined;
  3263. }
  3264. /**
  3265. * Add a component to the controller
  3266. * @param {Component} component
  3267. */
  3268. Controller.prototype.add = function add(component) {
  3269. // validate the component
  3270. if (component.id == undefined) {
  3271. throw new Error('Component has no field id');
  3272. }
  3273. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3274. throw new TypeError('Component must be an instance of ' +
  3275. 'prototype Component or Controller');
  3276. }
  3277. // add the component
  3278. component.controller = this;
  3279. this.components[component.id] = component;
  3280. };
  3281. /**
  3282. * Remove a component from the controller
  3283. * @param {Component | String} component
  3284. */
  3285. Controller.prototype.remove = function remove(component) {
  3286. var id;
  3287. for (id in this.components) {
  3288. if (this.components.hasOwnProperty(id)) {
  3289. if (id == component || this.components[id] == component) {
  3290. break;
  3291. }
  3292. }
  3293. }
  3294. if (id) {
  3295. delete this.components[id];
  3296. }
  3297. };
  3298. /**
  3299. * Request a reflow. The controller will schedule a reflow
  3300. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3301. * is false.
  3302. */
  3303. Controller.prototype.requestReflow = function requestReflow(force) {
  3304. if (force) {
  3305. this.reflow();
  3306. }
  3307. else {
  3308. if (!this.reflowTimer) {
  3309. var me = this;
  3310. this.reflowTimer = setTimeout(function () {
  3311. me.reflowTimer = undefined;
  3312. me.reflow();
  3313. }, 0);
  3314. }
  3315. }
  3316. };
  3317. /**
  3318. * Request a repaint. The controller will schedule a repaint
  3319. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3320. * is false.
  3321. */
  3322. Controller.prototype.requestRepaint = function requestRepaint(force) {
  3323. if (force) {
  3324. this.repaint();
  3325. }
  3326. else {
  3327. if (!this.repaintTimer) {
  3328. var me = this;
  3329. this.repaintTimer = setTimeout(function () {
  3330. me.repaintTimer = undefined;
  3331. me.repaint();
  3332. }, 0);
  3333. }
  3334. }
  3335. };
  3336. /**
  3337. * Repaint all components
  3338. */
  3339. Controller.prototype.repaint = function repaint() {
  3340. var changed = false;
  3341. // cancel any running repaint request
  3342. if (this.repaintTimer) {
  3343. clearTimeout(this.repaintTimer);
  3344. this.repaintTimer = undefined;
  3345. }
  3346. var done = {};
  3347. function repaint(component, id) {
  3348. if (!(id in done)) {
  3349. // first repaint the components on which this component is dependent
  3350. if (component.depends) {
  3351. component.depends.forEach(function (dep) {
  3352. repaint(dep, dep.id);
  3353. });
  3354. }
  3355. if (component.parent) {
  3356. repaint(component.parent, component.parent.id);
  3357. }
  3358. // repaint the component itself and mark as done
  3359. changed = component.repaint() || changed;
  3360. done[id] = true;
  3361. }
  3362. }
  3363. util.forEach(this.components, repaint);
  3364. // immediately reflow when needed
  3365. if (changed) {
  3366. this.reflow();
  3367. }
  3368. // TODO: limit the number of nested reflows/repaints, prevent loop
  3369. };
  3370. /**
  3371. * Reflow all components
  3372. */
  3373. Controller.prototype.reflow = function reflow() {
  3374. var resized = false;
  3375. // cancel any running repaint request
  3376. if (this.reflowTimer) {
  3377. clearTimeout(this.reflowTimer);
  3378. this.reflowTimer = undefined;
  3379. }
  3380. var done = {};
  3381. function reflow(component, id) {
  3382. if (!(id in done)) {
  3383. // first reflow the components on which this component is dependent
  3384. if (component.depends) {
  3385. component.depends.forEach(function (dep) {
  3386. reflow(dep, dep.id);
  3387. });
  3388. }
  3389. if (component.parent) {
  3390. reflow(component.parent, component.parent.id);
  3391. }
  3392. // reflow the component itself and mark as done
  3393. resized = component.reflow() || resized;
  3394. done[id] = true;
  3395. }
  3396. }
  3397. util.forEach(this.components, reflow);
  3398. // immediately repaint when needed
  3399. if (resized) {
  3400. this.repaint();
  3401. }
  3402. // TODO: limit the number of nested reflows/repaints, prevent loop
  3403. };
  3404. /**
  3405. * Prototype for visual components
  3406. */
  3407. function Component () {
  3408. this.id = null;
  3409. this.parent = null;
  3410. this.depends = null;
  3411. this.controller = null;
  3412. this.options = null;
  3413. this.frame = null; // main DOM element
  3414. this.top = 0;
  3415. this.left = 0;
  3416. this.width = 0;
  3417. this.height = 0;
  3418. }
  3419. /**
  3420. * Set parameters for the frame. Parameters will be merged in current parameter
  3421. * set.
  3422. * @param {Object} options Available parameters:
  3423. * {String | function} [className]
  3424. * {EventBus} [eventBus]
  3425. * {String | Number | function} [left]
  3426. * {String | Number | function} [top]
  3427. * {String | Number | function} [width]
  3428. * {String | Number | function} [height]
  3429. */
  3430. Component.prototype.setOptions = function setOptions(options) {
  3431. if (options) {
  3432. util.extend(this.options, options);
  3433. if (this.controller) {
  3434. this.requestRepaint();
  3435. this.requestReflow();
  3436. }
  3437. }
  3438. };
  3439. /**
  3440. * Get an option value by name
  3441. * The function will first check this.options object, and else will check
  3442. * this.defaultOptions.
  3443. * @param {String} name
  3444. * @return {*} value
  3445. */
  3446. Component.prototype.getOption = function getOption(name) {
  3447. var value;
  3448. if (this.options) {
  3449. value = this.options[name];
  3450. }
  3451. if (value === undefined && this.defaultOptions) {
  3452. value = this.defaultOptions[name];
  3453. }
  3454. return value;
  3455. };
  3456. /**
  3457. * Get the container element of the component, which can be used by a child to
  3458. * add its own widgets. Not all components do have a container for childs, in
  3459. * that case null is returned.
  3460. * @returns {HTMLElement | null} container
  3461. */
  3462. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  3463. Component.prototype.getContainer = function getContainer() {
  3464. // should be implemented by the component
  3465. return null;
  3466. };
  3467. /**
  3468. * Get the frame element of the component, the outer HTML DOM element.
  3469. * @returns {HTMLElement | null} frame
  3470. */
  3471. Component.prototype.getFrame = function getFrame() {
  3472. return this.frame;
  3473. };
  3474. /**
  3475. * Repaint the component
  3476. * @return {Boolean} changed
  3477. */
  3478. Component.prototype.repaint = function repaint() {
  3479. // should be implemented by the component
  3480. return false;
  3481. };
  3482. /**
  3483. * Reflow the component
  3484. * @return {Boolean} resized
  3485. */
  3486. Component.prototype.reflow = function reflow() {
  3487. // should be implemented by the component
  3488. return false;
  3489. };
  3490. /**
  3491. * Hide the component from the DOM
  3492. * @return {Boolean} changed
  3493. */
  3494. Component.prototype.hide = function hide() {
  3495. if (this.frame && this.frame.parentNode) {
  3496. this.frame.parentNode.removeChild(this.frame);
  3497. return true;
  3498. }
  3499. else {
  3500. return false;
  3501. }
  3502. };
  3503. /**
  3504. * Show the component in the DOM (when not already visible).
  3505. * A repaint will be executed when the component is not visible
  3506. * @return {Boolean} changed
  3507. */
  3508. Component.prototype.show = function show() {
  3509. if (!this.frame || !this.frame.parentNode) {
  3510. return this.repaint();
  3511. }
  3512. else {
  3513. return false;
  3514. }
  3515. };
  3516. /**
  3517. * Request a repaint. The controller will schedule a repaint
  3518. */
  3519. Component.prototype.requestRepaint = function requestRepaint() {
  3520. if (this.controller) {
  3521. this.controller.requestRepaint();
  3522. }
  3523. else {
  3524. throw new Error('Cannot request a repaint: no controller configured');
  3525. // TODO: just do a repaint when no parent is configured?
  3526. }
  3527. };
  3528. /**
  3529. * Request a reflow. The controller will schedule a reflow
  3530. */
  3531. Component.prototype.requestReflow = function requestReflow() {
  3532. if (this.controller) {
  3533. this.controller.requestReflow();
  3534. }
  3535. else {
  3536. throw new Error('Cannot request a reflow: no controller configured');
  3537. // TODO: just do a reflow when no parent is configured?
  3538. }
  3539. };
  3540. /**
  3541. * A panel can contain components
  3542. * @param {Component} [parent]
  3543. * @param {Component[]} [depends] Components on which this components depends
  3544. * (except for the parent)
  3545. * @param {Object} [options] Available parameters:
  3546. * {String | Number | function} [left]
  3547. * {String | Number | function} [top]
  3548. * {String | Number | function} [width]
  3549. * {String | Number | function} [height]
  3550. * {String | function} [className]
  3551. * @constructor Panel
  3552. * @extends Component
  3553. */
  3554. function Panel(parent, depends, options) {
  3555. this.id = util.randomUUID();
  3556. this.parent = parent;
  3557. this.depends = depends;
  3558. this.options = options || {};
  3559. }
  3560. Panel.prototype = new Component();
  3561. /**
  3562. * Set options. Will extend the current options.
  3563. * @param {Object} [options] Available parameters:
  3564. * {String | function} [className]
  3565. * {String | Number | function} [left]
  3566. * {String | Number | function} [top]
  3567. * {String | Number | function} [width]
  3568. * {String | Number | function} [height]
  3569. */
  3570. Panel.prototype.setOptions = Component.prototype.setOptions;
  3571. /**
  3572. * Get the container element of the panel, which can be used by a child to
  3573. * add its own widgets.
  3574. * @returns {HTMLElement} container
  3575. */
  3576. Panel.prototype.getContainer = function () {
  3577. return this.frame;
  3578. };
  3579. /**
  3580. * Repaint the component
  3581. * @return {Boolean} changed
  3582. */
  3583. Panel.prototype.repaint = function () {
  3584. var changed = 0,
  3585. update = util.updateProperty,
  3586. asSize = util.option.asSize,
  3587. options = this.options,
  3588. frame = this.frame;
  3589. if (!frame) {
  3590. frame = document.createElement('div');
  3591. frame.className = 'panel';
  3592. var className = options.className;
  3593. if (className) {
  3594. if (typeof className == 'function') {
  3595. util.addClassName(frame, String(className()));
  3596. }
  3597. else {
  3598. util.addClassName(frame, String(className));
  3599. }
  3600. }
  3601. this.frame = frame;
  3602. changed += 1;
  3603. }
  3604. if (!frame.parentNode) {
  3605. if (!this.parent) {
  3606. throw new Error('Cannot repaint panel: no parent attached');
  3607. }
  3608. var parentContainer = this.parent.getContainer();
  3609. if (!parentContainer) {
  3610. throw new Error('Cannot repaint panel: parent has no container element');
  3611. }
  3612. parentContainer.appendChild(frame);
  3613. changed += 1;
  3614. }
  3615. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3616. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3617. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3618. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3619. return (changed > 0);
  3620. };
  3621. /**
  3622. * Reflow the component
  3623. * @return {Boolean} resized
  3624. */
  3625. Panel.prototype.reflow = function () {
  3626. var changed = 0,
  3627. update = util.updateProperty,
  3628. frame = this.frame;
  3629. if (frame) {
  3630. changed += update(this, 'top', frame.offsetTop);
  3631. changed += update(this, 'left', frame.offsetLeft);
  3632. changed += update(this, 'width', frame.offsetWidth);
  3633. changed += update(this, 'height', frame.offsetHeight);
  3634. }
  3635. else {
  3636. changed += 1;
  3637. }
  3638. return (changed > 0);
  3639. };
  3640. /**
  3641. * A root panel can hold components. The root panel must be initialized with
  3642. * a DOM element as container.
  3643. * @param {HTMLElement} container
  3644. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3645. * @constructor RootPanel
  3646. * @extends Panel
  3647. */
  3648. function RootPanel(container, options) {
  3649. this.id = util.randomUUID();
  3650. this.container = container;
  3651. this.options = options || {};
  3652. this.defaultOptions = {
  3653. autoResize: true
  3654. };
  3655. this.listeners = {}; // event listeners
  3656. }
  3657. RootPanel.prototype = new Panel();
  3658. /**
  3659. * Set options. Will extend the current options.
  3660. * @param {Object} [options] Available parameters:
  3661. * {String | function} [className]
  3662. * {String | Number | function} [left]
  3663. * {String | Number | function} [top]
  3664. * {String | Number | function} [width]
  3665. * {String | Number | function} [height]
  3666. * {Boolean | function} [autoResize]
  3667. */
  3668. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3669. /**
  3670. * Repaint the component
  3671. * @return {Boolean} changed
  3672. */
  3673. RootPanel.prototype.repaint = function () {
  3674. var changed = 0,
  3675. update = util.updateProperty,
  3676. asSize = util.option.asSize,
  3677. options = this.options,
  3678. frame = this.frame;
  3679. if (!frame) {
  3680. frame = document.createElement('div');
  3681. this.frame = frame;
  3682. changed += 1;
  3683. }
  3684. if (!frame.parentNode) {
  3685. if (!this.container) {
  3686. throw new Error('Cannot repaint root panel: no container attached');
  3687. }
  3688. this.container.appendChild(frame);
  3689. changed += 1;
  3690. }
  3691. frame.className = 'vis timeline rootpanel ' + options.orientation;
  3692. var className = options.className;
  3693. if (className) {
  3694. util.addClassName(frame, util.option.asString(className));
  3695. }
  3696. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3697. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3698. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3699. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3700. this._updateEventEmitters();
  3701. this._updateWatch();
  3702. return (changed > 0);
  3703. };
  3704. /**
  3705. * Reflow the component
  3706. * @return {Boolean} resized
  3707. */
  3708. RootPanel.prototype.reflow = function () {
  3709. var changed = 0,
  3710. update = util.updateProperty,
  3711. frame = this.frame;
  3712. if (frame) {
  3713. changed += update(this, 'top', frame.offsetTop);
  3714. changed += update(this, 'left', frame.offsetLeft);
  3715. changed += update(this, 'width', frame.offsetWidth);
  3716. changed += update(this, 'height', frame.offsetHeight);
  3717. }
  3718. else {
  3719. changed += 1;
  3720. }
  3721. return (changed > 0);
  3722. };
  3723. /**
  3724. * Update watching for resize, depending on the current option
  3725. * @private
  3726. */
  3727. RootPanel.prototype._updateWatch = function () {
  3728. var autoResize = this.getOption('autoResize');
  3729. if (autoResize) {
  3730. this._watch();
  3731. }
  3732. else {
  3733. this._unwatch();
  3734. }
  3735. };
  3736. /**
  3737. * Watch for changes in the size of the frame. On resize, the Panel will
  3738. * automatically redraw itself.
  3739. * @private
  3740. */
  3741. RootPanel.prototype._watch = function () {
  3742. var me = this;
  3743. this._unwatch();
  3744. var checkSize = function () {
  3745. var autoResize = me.getOption('autoResize');
  3746. if (!autoResize) {
  3747. // stop watching when the option autoResize is changed to false
  3748. me._unwatch();
  3749. return;
  3750. }
  3751. if (me.frame) {
  3752. // check whether the frame is resized
  3753. if ((me.frame.clientWidth != me.width) ||
  3754. (me.frame.clientHeight != me.height)) {
  3755. me.requestReflow();
  3756. }
  3757. }
  3758. };
  3759. // TODO: automatically cleanup the event listener when the frame is deleted
  3760. util.addEventListener(window, 'resize', checkSize);
  3761. this.watchTimer = setInterval(checkSize, 1000);
  3762. };
  3763. /**
  3764. * Stop watching for a resize of the frame.
  3765. * @private
  3766. */
  3767. RootPanel.prototype._unwatch = function () {
  3768. if (this.watchTimer) {
  3769. clearInterval(this.watchTimer);
  3770. this.watchTimer = undefined;
  3771. }
  3772. // TODO: remove event listener on window.resize
  3773. };
  3774. /**
  3775. * Event handler
  3776. * @param {String} event name of the event, for example 'click', 'mousemove'
  3777. * @param {function} callback callback handler, invoked with the raw HTML Event
  3778. * as parameter.
  3779. */
  3780. RootPanel.prototype.on = function (event, callback) {
  3781. // register the listener at this component
  3782. var arr = this.listeners[event];
  3783. if (!arr) {
  3784. arr = [];
  3785. this.listeners[event] = arr;
  3786. }
  3787. arr.push(callback);
  3788. this._updateEventEmitters();
  3789. };
  3790. /**
  3791. * Update the event listeners for all event emitters
  3792. * @private
  3793. */
  3794. RootPanel.prototype._updateEventEmitters = function () {
  3795. if (this.listeners) {
  3796. var me = this;
  3797. util.forEach(this.listeners, function (listeners, event) {
  3798. if (!me.emitters) {
  3799. me.emitters = {};
  3800. }
  3801. if (!(event in me.emitters)) {
  3802. // create event
  3803. var frame = me.frame;
  3804. if (frame) {
  3805. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3806. var callback = function(event) {
  3807. listeners.forEach(function (listener) {
  3808. // TODO: filter on event target!
  3809. listener(event);
  3810. });
  3811. };
  3812. me.emitters[event] = callback;
  3813. if (!me.hammer) {
  3814. me.hammer = Hammer(frame, {
  3815. prevent_default: true
  3816. });
  3817. }
  3818. me.hammer.on(event, callback);
  3819. }
  3820. }
  3821. });
  3822. // TODO: be able to delete event listeners
  3823. // TODO: be able to move event listeners to a parent when available
  3824. }
  3825. };
  3826. /**
  3827. * A horizontal time axis
  3828. * @param {Component} parent
  3829. * @param {Component[]} [depends] Components on which this components depends
  3830. * (except for the parent)
  3831. * @param {Object} [options] See TimeAxis.setOptions for the available
  3832. * options.
  3833. * @constructor TimeAxis
  3834. * @extends Component
  3835. */
  3836. function TimeAxis (parent, depends, options) {
  3837. this.id = util.randomUUID();
  3838. this.parent = parent;
  3839. this.depends = depends;
  3840. this.dom = {
  3841. majorLines: [],
  3842. majorTexts: [],
  3843. minorLines: [],
  3844. minorTexts: [],
  3845. redundant: {
  3846. majorLines: [],
  3847. majorTexts: [],
  3848. minorLines: [],
  3849. minorTexts: []
  3850. }
  3851. };
  3852. this.props = {
  3853. range: {
  3854. start: 0,
  3855. end: 0,
  3856. minimumStep: 0
  3857. },
  3858. lineTop: 0
  3859. };
  3860. this.options = options || {};
  3861. this.defaultOptions = {
  3862. orientation: 'bottom', // supported: 'top', 'bottom'
  3863. // TODO: implement timeaxis orientations 'left' and 'right'
  3864. showMinorLabels: true,
  3865. showMajorLabels: true
  3866. };
  3867. this.conversion = null;
  3868. this.range = null;
  3869. }
  3870. TimeAxis.prototype = new Component();
  3871. // TODO: comment options
  3872. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3873. /**
  3874. * Set a range (start and end)
  3875. * @param {Range | Object} range A Range or an object containing start and end.
  3876. */
  3877. TimeAxis.prototype.setRange = function (range) {
  3878. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3879. throw new TypeError('Range must be an instance of Range, ' +
  3880. 'or an object containing start and end.');
  3881. }
  3882. this.range = range;
  3883. };
  3884. /**
  3885. * Convert a position on screen (pixels) to a datetime
  3886. * @param {int} x Position on the screen in pixels
  3887. * @return {Date} time The datetime the corresponds with given position x
  3888. */
  3889. TimeAxis.prototype.toTime = function(x) {
  3890. var conversion = this.conversion;
  3891. return new Date(x / conversion.scale + conversion.offset);
  3892. };
  3893. /**
  3894. * Convert a datetime (Date object) into a position on the screen
  3895. * @param {Date} time A date
  3896. * @return {int} x The position on the screen in pixels which corresponds
  3897. * with the given date.
  3898. * @private
  3899. */
  3900. TimeAxis.prototype.toScreen = function(time) {
  3901. var conversion = this.conversion;
  3902. return (time.valueOf() - conversion.offset) * conversion.scale;
  3903. };
  3904. /**
  3905. * Repaint the component
  3906. * @return {Boolean} changed
  3907. */
  3908. TimeAxis.prototype.repaint = function () {
  3909. var changed = 0,
  3910. update = util.updateProperty,
  3911. asSize = util.option.asSize,
  3912. options = this.options,
  3913. orientation = this.getOption('orientation'),
  3914. props = this.props,
  3915. step = this.step;
  3916. var frame = this.frame;
  3917. if (!frame) {
  3918. frame = document.createElement('div');
  3919. this.frame = frame;
  3920. changed += 1;
  3921. }
  3922. frame.className = 'axis';
  3923. // TODO: custom className?
  3924. if (!frame.parentNode) {
  3925. if (!this.parent) {
  3926. throw new Error('Cannot repaint time axis: no parent attached');
  3927. }
  3928. var parentContainer = this.parent.getContainer();
  3929. if (!parentContainer) {
  3930. throw new Error('Cannot repaint time axis: parent has no container element');
  3931. }
  3932. parentContainer.appendChild(frame);
  3933. changed += 1;
  3934. }
  3935. var parent = frame.parentNode;
  3936. if (parent) {
  3937. var beforeChild = frame.nextSibling;
  3938. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3939. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3940. (this.props.parentHeight - this.height) + 'px' :
  3941. '0px';
  3942. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3943. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3944. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3945. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3946. // get characters width and height
  3947. this._repaintMeasureChars();
  3948. if (this.step) {
  3949. this._repaintStart();
  3950. step.first();
  3951. var xFirstMajorLabel = undefined;
  3952. var max = 0;
  3953. while (step.hasNext() && max < 1000) {
  3954. max++;
  3955. var cur = step.getCurrent(),
  3956. x = this.toScreen(cur),
  3957. isMajor = step.isMajor();
  3958. // TODO: lines must have a width, such that we can create css backgrounds
  3959. if (this.getOption('showMinorLabels')) {
  3960. this._repaintMinorText(x, step.getLabelMinor());
  3961. }
  3962. if (isMajor && this.getOption('showMajorLabels')) {
  3963. if (x > 0) {
  3964. if (xFirstMajorLabel == undefined) {
  3965. xFirstMajorLabel = x;
  3966. }
  3967. this._repaintMajorText(x, step.getLabelMajor());
  3968. }
  3969. this._repaintMajorLine(x);
  3970. }
  3971. else {
  3972. this._repaintMinorLine(x);
  3973. }
  3974. step.next();
  3975. }
  3976. // create a major label on the left when needed
  3977. if (this.getOption('showMajorLabels')) {
  3978. var leftTime = this.toTime(0),
  3979. leftText = step.getLabelMajor(leftTime),
  3980. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3981. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3982. this._repaintMajorText(0, leftText);
  3983. }
  3984. }
  3985. this._repaintEnd();
  3986. }
  3987. this._repaintLine();
  3988. // put frame online again
  3989. if (beforeChild) {
  3990. parent.insertBefore(frame, beforeChild);
  3991. }
  3992. else {
  3993. parent.appendChild(frame)
  3994. }
  3995. }
  3996. return (changed > 0);
  3997. };
  3998. /**
  3999. * Start a repaint. Move all DOM elements to a redundant list, where they
  4000. * can be picked for re-use, or can be cleaned up in the end
  4001. * @private
  4002. */
  4003. TimeAxis.prototype._repaintStart = function () {
  4004. var dom = this.dom,
  4005. redundant = dom.redundant;
  4006. redundant.majorLines = dom.majorLines;
  4007. redundant.majorTexts = dom.majorTexts;
  4008. redundant.minorLines = dom.minorLines;
  4009. redundant.minorTexts = dom.minorTexts;
  4010. dom.majorLines = [];
  4011. dom.majorTexts = [];
  4012. dom.minorLines = [];
  4013. dom.minorTexts = [];
  4014. };
  4015. /**
  4016. * End a repaint. Cleanup leftover DOM elements in the redundant list
  4017. * @private
  4018. */
  4019. TimeAxis.prototype._repaintEnd = function () {
  4020. util.forEach(this.dom.redundant, function (arr) {
  4021. while (arr.length) {
  4022. var elem = arr.pop();
  4023. if (elem && elem.parentNode) {
  4024. elem.parentNode.removeChild(elem);
  4025. }
  4026. }
  4027. });
  4028. };
  4029. /**
  4030. * Create a minor label for the axis at position x
  4031. * @param {Number} x
  4032. * @param {String} text
  4033. * @private
  4034. */
  4035. TimeAxis.prototype._repaintMinorText = function (x, text) {
  4036. // reuse redundant label
  4037. var label = this.dom.redundant.minorTexts.shift();
  4038. if (!label) {
  4039. // create new label
  4040. var content = document.createTextNode('');
  4041. label = document.createElement('div');
  4042. label.appendChild(content);
  4043. label.className = 'text minor';
  4044. this.frame.appendChild(label);
  4045. }
  4046. this.dom.minorTexts.push(label);
  4047. label.childNodes[0].nodeValue = text;
  4048. label.style.left = x + 'px';
  4049. label.style.top = this.props.minorLabelTop + 'px';
  4050. //label.title = title; // TODO: this is a heavy operation
  4051. };
  4052. /**
  4053. * Create a Major label for the axis at position x
  4054. * @param {Number} x
  4055. * @param {String} text
  4056. * @private
  4057. */
  4058. TimeAxis.prototype._repaintMajorText = function (x, text) {
  4059. // reuse redundant label
  4060. var label = this.dom.redundant.majorTexts.shift();
  4061. if (!label) {
  4062. // create label
  4063. var content = document.createTextNode(text);
  4064. label = document.createElement('div');
  4065. label.className = 'text major';
  4066. label.appendChild(content);
  4067. this.frame.appendChild(label);
  4068. }
  4069. this.dom.majorTexts.push(label);
  4070. label.childNodes[0].nodeValue = text;
  4071. label.style.top = this.props.majorLabelTop + 'px';
  4072. label.style.left = x + 'px';
  4073. //label.title = title; // TODO: this is a heavy operation
  4074. };
  4075. /**
  4076. * Create a minor line for the axis at position x
  4077. * @param {Number} x
  4078. * @private
  4079. */
  4080. TimeAxis.prototype._repaintMinorLine = function (x) {
  4081. // reuse redundant line
  4082. var line = this.dom.redundant.minorLines.shift();
  4083. if (!line) {
  4084. // create vertical line
  4085. line = document.createElement('div');
  4086. line.className = 'grid vertical minor';
  4087. this.frame.appendChild(line);
  4088. }
  4089. this.dom.minorLines.push(line);
  4090. var props = this.props;
  4091. line.style.top = props.minorLineTop + 'px';
  4092. line.style.height = props.minorLineHeight + 'px';
  4093. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4094. };
  4095. /**
  4096. * Create a Major line for the axis at position x
  4097. * @param {Number} x
  4098. * @private
  4099. */
  4100. TimeAxis.prototype._repaintMajorLine = function (x) {
  4101. // reuse redundant line
  4102. var line = this.dom.redundant.majorLines.shift();
  4103. if (!line) {
  4104. // create vertical line
  4105. line = document.createElement('DIV');
  4106. line.className = 'grid vertical major';
  4107. this.frame.appendChild(line);
  4108. }
  4109. this.dom.majorLines.push(line);
  4110. var props = this.props;
  4111. line.style.top = props.majorLineTop + 'px';
  4112. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4113. line.style.height = props.majorLineHeight + 'px';
  4114. };
  4115. /**
  4116. * Repaint the horizontal line for the axis
  4117. * @private
  4118. */
  4119. TimeAxis.prototype._repaintLine = function() {
  4120. var line = this.dom.line,
  4121. frame = this.frame,
  4122. options = this.options;
  4123. // line before all axis elements
  4124. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4125. if (line) {
  4126. // put this line at the end of all childs
  4127. frame.removeChild(line);
  4128. frame.appendChild(line);
  4129. }
  4130. else {
  4131. // create the axis line
  4132. line = document.createElement('div');
  4133. line.className = 'grid horizontal major';
  4134. frame.appendChild(line);
  4135. this.dom.line = line;
  4136. }
  4137. line.style.top = this.props.lineTop + 'px';
  4138. }
  4139. else {
  4140. if (line && line.parentElement) {
  4141. frame.removeChild(line.line);
  4142. delete this.dom.line;
  4143. }
  4144. }
  4145. };
  4146. /**
  4147. * Create characters used to determine the size of text on the axis
  4148. * @private
  4149. */
  4150. TimeAxis.prototype._repaintMeasureChars = function () {
  4151. // calculate the width and height of a single character
  4152. // this is used to calculate the step size, and also the positioning of the
  4153. // axis
  4154. var dom = this.dom,
  4155. text;
  4156. if (!dom.measureCharMinor) {
  4157. text = document.createTextNode('0');
  4158. var measureCharMinor = document.createElement('DIV');
  4159. measureCharMinor.className = 'text minor measure';
  4160. measureCharMinor.appendChild(text);
  4161. this.frame.appendChild(measureCharMinor);
  4162. dom.measureCharMinor = measureCharMinor;
  4163. }
  4164. if (!dom.measureCharMajor) {
  4165. text = document.createTextNode('0');
  4166. var measureCharMajor = document.createElement('DIV');
  4167. measureCharMajor.className = 'text major measure';
  4168. measureCharMajor.appendChild(text);
  4169. this.frame.appendChild(measureCharMajor);
  4170. dom.measureCharMajor = measureCharMajor;
  4171. }
  4172. };
  4173. /**
  4174. * Reflow the component
  4175. * @return {Boolean} resized
  4176. */
  4177. TimeAxis.prototype.reflow = function () {
  4178. var changed = 0,
  4179. update = util.updateProperty,
  4180. frame = this.frame,
  4181. range = this.range;
  4182. if (!range) {
  4183. throw new Error('Cannot repaint time axis: no range configured');
  4184. }
  4185. if (frame) {
  4186. changed += update(this, 'top', frame.offsetTop);
  4187. changed += update(this, 'left', frame.offsetLeft);
  4188. // calculate size of a character
  4189. var props = this.props,
  4190. showMinorLabels = this.getOption('showMinorLabels'),
  4191. showMajorLabels = this.getOption('showMajorLabels'),
  4192. measureCharMinor = this.dom.measureCharMinor,
  4193. measureCharMajor = this.dom.measureCharMajor;
  4194. if (measureCharMinor) {
  4195. props.minorCharHeight = measureCharMinor.clientHeight;
  4196. props.minorCharWidth = measureCharMinor.clientWidth;
  4197. }
  4198. if (measureCharMajor) {
  4199. props.majorCharHeight = measureCharMajor.clientHeight;
  4200. props.majorCharWidth = measureCharMajor.clientWidth;
  4201. }
  4202. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4203. if (parentHeight != props.parentHeight) {
  4204. props.parentHeight = parentHeight;
  4205. changed += 1;
  4206. }
  4207. switch (this.getOption('orientation')) {
  4208. case 'bottom':
  4209. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4210. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4211. props.minorLabelTop = 0;
  4212. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4213. props.minorLineTop = -this.top;
  4214. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4215. props.minorLineWidth = 1; // TODO: really calculate width
  4216. props.majorLineTop = -this.top;
  4217. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4218. props.majorLineWidth = 1; // TODO: really calculate width
  4219. props.lineTop = 0;
  4220. break;
  4221. case 'top':
  4222. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4223. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4224. props.majorLabelTop = 0;
  4225. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4226. props.minorLineTop = props.minorLabelTop;
  4227. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4228. props.minorLineWidth = 1; // TODO: really calculate width
  4229. props.majorLineTop = 0;
  4230. props.majorLineHeight = Math.max(parentHeight - this.top);
  4231. props.majorLineWidth = 1; // TODO: really calculate width
  4232. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4233. break;
  4234. default:
  4235. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4236. }
  4237. var height = props.minorLabelHeight + props.majorLabelHeight;
  4238. changed += update(this, 'width', frame.offsetWidth);
  4239. changed += update(this, 'height', height);
  4240. // calculate range and step
  4241. this._updateConversion();
  4242. var start = util.convert(range.start, 'Number'),
  4243. end = util.convert(range.end, 'Number'),
  4244. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  4245. -this.toTime(0).valueOf();
  4246. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  4247. changed += update(props.range, 'start', start);
  4248. changed += update(props.range, 'end', end);
  4249. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4250. }
  4251. return (changed > 0);
  4252. };
  4253. /**
  4254. * Calculate the scale and offset to convert a position on screen to the
  4255. * corresponding date and vice versa.
  4256. * After the method _updateConversion is executed once, the methods toTime
  4257. * and toScreen can be used.
  4258. * @private
  4259. */
  4260. TimeAxis.prototype._updateConversion = function() {
  4261. var range = this.range;
  4262. if (!range) {
  4263. throw new Error('No range configured');
  4264. }
  4265. if (range.conversion) {
  4266. this.conversion = range.conversion(this.width);
  4267. }
  4268. else {
  4269. this.conversion = Range.conversion(range.start, range.end, this.width);
  4270. }
  4271. };
  4272. /**
  4273. * A current time bar
  4274. * @param {Component} parent
  4275. * @param {Component[]} [depends] Components on which this components depends
  4276. * (except for the parent)
  4277. * @param {Object} [options] Available parameters:
  4278. * {Boolean} [showCurrentTime]
  4279. * @constructor CurrentTime
  4280. * @extends Component
  4281. */
  4282. function CurrentTime (parent, depends, options) {
  4283. this.id = util.randomUUID();
  4284. this.parent = parent;
  4285. this.depends = depends;
  4286. this.options = options || {};
  4287. this.defaultOptions = {
  4288. showCurrentTime: false
  4289. };
  4290. }
  4291. CurrentTime.prototype = new Component();
  4292. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  4293. /**
  4294. * Get the container element of the bar, which can be used by a child to
  4295. * add its own widgets.
  4296. * @returns {HTMLElement} container
  4297. */
  4298. CurrentTime.prototype.getContainer = function () {
  4299. return this.frame;
  4300. };
  4301. /**
  4302. * Repaint the component
  4303. * @return {Boolean} changed
  4304. */
  4305. CurrentTime.prototype.repaint = function () {
  4306. var bar = this.frame,
  4307. parent = this.parent,
  4308. parentContainer = parent.parent.getContainer();
  4309. if (!parent) {
  4310. throw new Error('Cannot repaint bar: no parent attached');
  4311. }
  4312. if (!parentContainer) {
  4313. throw new Error('Cannot repaint bar: parent has no container element');
  4314. }
  4315. if (!this.getOption('showCurrentTime')) {
  4316. if (bar) {
  4317. parentContainer.removeChild(bar);
  4318. delete this.frame;
  4319. }
  4320. return;
  4321. }
  4322. if (!bar) {
  4323. bar = document.createElement('div');
  4324. bar.className = 'currenttime';
  4325. bar.style.position = 'absolute';
  4326. bar.style.top = '0px';
  4327. bar.style.height = '100%';
  4328. parentContainer.appendChild(bar);
  4329. this.frame = bar;
  4330. }
  4331. if (!parent.conversion) {
  4332. parent._updateConversion();
  4333. }
  4334. var now = new Date();
  4335. var x = parent.toScreen(now);
  4336. bar.style.left = x + 'px';
  4337. bar.title = 'Current time: ' + now;
  4338. // start a timer to adjust for the new time
  4339. if (this.currentTimeTimer !== undefined) {
  4340. clearTimeout(this.currentTimeTimer);
  4341. delete this.currentTimeTimer;
  4342. }
  4343. var timeline = this;
  4344. var interval = 1 / parent.conversion.scale / 2;
  4345. if (interval < 30) {
  4346. interval = 30;
  4347. }
  4348. this.currentTimeTimer = setTimeout(function() {
  4349. timeline.repaint();
  4350. }, interval);
  4351. return false;
  4352. };
  4353. /**
  4354. * A custom time bar
  4355. * @param {Component} parent
  4356. * @param {Component[]} [depends] Components on which this components depends
  4357. * (except for the parent)
  4358. * @param {Object} [options] Available parameters:
  4359. * {Boolean} [showCustomTime]
  4360. * @constructor CustomTime
  4361. * @extends Component
  4362. */
  4363. function CustomTime (parent, depends, options) {
  4364. this.id = util.randomUUID();
  4365. this.parent = parent;
  4366. this.depends = depends;
  4367. this.options = options || {};
  4368. this.defaultOptions = {
  4369. showCustomTime: false
  4370. };
  4371. this.listeners = [];
  4372. this.customTime = new Date();
  4373. }
  4374. CustomTime.prototype = new Component();
  4375. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4376. /**
  4377. * Get the container element of the bar, which can be used by a child to
  4378. * add its own widgets.
  4379. * @returns {HTMLElement} container
  4380. */
  4381. CustomTime.prototype.getContainer = function () {
  4382. return this.frame;
  4383. };
  4384. /**
  4385. * Repaint the component
  4386. * @return {Boolean} changed
  4387. */
  4388. CustomTime.prototype.repaint = function () {
  4389. var bar = this.frame,
  4390. parent = this.parent,
  4391. parentContainer = parent.parent.getContainer();
  4392. if (!parent) {
  4393. throw new Error('Cannot repaint bar: no parent attached');
  4394. }
  4395. if (!parentContainer) {
  4396. throw new Error('Cannot repaint bar: parent has no container element');
  4397. }
  4398. if (!this.getOption('showCustomTime')) {
  4399. if (bar) {
  4400. parentContainer.removeChild(bar);
  4401. delete this.frame;
  4402. }
  4403. return;
  4404. }
  4405. if (!bar) {
  4406. bar = document.createElement('div');
  4407. bar.className = 'customtime';
  4408. bar.style.position = 'absolute';
  4409. bar.style.top = '0px';
  4410. bar.style.height = '100%';
  4411. parentContainer.appendChild(bar);
  4412. var drag = document.createElement('div');
  4413. drag.style.position = 'relative';
  4414. drag.style.top = '0px';
  4415. drag.style.left = '-10px';
  4416. drag.style.height = '100%';
  4417. drag.style.width = '20px';
  4418. bar.appendChild(drag);
  4419. this.frame = bar;
  4420. this.subscribe(this, 'movetime');
  4421. }
  4422. if (!parent.conversion) {
  4423. parent._updateConversion();
  4424. }
  4425. var x = parent.toScreen(this.customTime);
  4426. bar.style.left = x + 'px';
  4427. bar.title = 'Time: ' + this.customTime;
  4428. return false;
  4429. };
  4430. /**
  4431. * Set custom time.
  4432. * @param {Date} time
  4433. */
  4434. CustomTime.prototype._setCustomTime = function(time) {
  4435. this.customTime = new Date(time.valueOf());
  4436. this.repaint();
  4437. };
  4438. /**
  4439. * Retrieve the current custom time.
  4440. * @return {Date} customTime
  4441. */
  4442. CustomTime.prototype._getCustomTime = function() {
  4443. return new Date(this.customTime.valueOf());
  4444. };
  4445. /**
  4446. * Add listeners for mouse and touch events to the component
  4447. * @param {Component} component
  4448. */
  4449. CustomTime.prototype.subscribe = function (component, event) {
  4450. var me = this;
  4451. var listener = {
  4452. component: component,
  4453. event: event,
  4454. callback: function (event) {
  4455. me._onMouseDown(event, listener);
  4456. },
  4457. params: {}
  4458. };
  4459. component.on('mousedown', listener.callback);
  4460. me.listeners.push(listener);
  4461. };
  4462. /**
  4463. * Event handler
  4464. * @param {String} event name of the event, for example 'click', 'mousemove'
  4465. * @param {function} callback callback handler, invoked with the raw HTML Event
  4466. * as parameter.
  4467. */
  4468. CustomTime.prototype.on = function (event, callback) {
  4469. var bar = this.frame;
  4470. if (!bar) {
  4471. throw new Error('Cannot add event listener: no parent attached');
  4472. }
  4473. events.addListener(this, event, callback);
  4474. util.addEventListener(bar, event, callback);
  4475. };
  4476. /**
  4477. * Start moving horizontally
  4478. * @param {Event} event
  4479. * @param {Object} listener Listener containing the component and params
  4480. * @private
  4481. */
  4482. CustomTime.prototype._onMouseDown = function(event, listener) {
  4483. event = event || window.event;
  4484. var params = listener.params;
  4485. // only react on left mouse button down
  4486. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  4487. if (!leftButtonDown) {
  4488. return;
  4489. }
  4490. // get mouse position
  4491. params.mouseX = util.getPageX(event);
  4492. params.moved = false;
  4493. params.customTime = this.customTime;
  4494. // add event listeners to handle moving the custom time bar
  4495. var me = this;
  4496. if (!params.onMouseMove) {
  4497. params.onMouseMove = function (event) {
  4498. me._onMouseMove(event, listener);
  4499. };
  4500. util.addEventListener(document, 'mousemove', params.onMouseMove);
  4501. }
  4502. if (!params.onMouseUp) {
  4503. params.onMouseUp = function (event) {
  4504. me._onMouseUp(event, listener);
  4505. };
  4506. util.addEventListener(document, 'mouseup', params.onMouseUp);
  4507. }
  4508. util.stopPropagation(event);
  4509. util.preventDefault(event);
  4510. };
  4511. /**
  4512. * Perform moving operating.
  4513. * This function activated from within the funcion CustomTime._onMouseDown().
  4514. * @param {Event} event
  4515. * @param {Object} listener
  4516. * @private
  4517. */
  4518. CustomTime.prototype._onMouseMove = function (event, listener) {
  4519. event = event || window.event;
  4520. var params = listener.params;
  4521. var parent = this.parent;
  4522. // calculate change in mouse position
  4523. var mouseX = util.getPageX(event);
  4524. if (params.mouseX === undefined) {
  4525. params.mouseX = mouseX;
  4526. }
  4527. var diff = mouseX - params.mouseX;
  4528. // if mouse movement is big enough, register it as a "moved" event
  4529. if (Math.abs(diff) >= 1) {
  4530. params.moved = true;
  4531. }
  4532. var x = parent.toScreen(params.customTime);
  4533. var xnew = x + diff;
  4534. var time = parent.toTime(xnew);
  4535. this._setCustomTime(time);
  4536. // fire a timechange event
  4537. events.trigger(this, 'timechange', {customTime: this.customTime});
  4538. util.preventDefault(event);
  4539. };
  4540. /**
  4541. * Stop moving operating.
  4542. * This function activated from within the function CustomTime._onMouseDown().
  4543. * @param {event} event
  4544. * @param {Object} listener
  4545. * @private
  4546. */
  4547. CustomTime.prototype._onMouseUp = function (event, listener) {
  4548. event = event || window.event;
  4549. var params = listener.params;
  4550. // remove event listeners here, important for Safari
  4551. if (params.onMouseMove) {
  4552. util.removeEventListener(document, 'mousemove', params.onMouseMove);
  4553. params.onMouseMove = null;
  4554. }
  4555. if (params.onMouseUp) {
  4556. util.removeEventListener(document, 'mouseup', params.onMouseUp);
  4557. params.onMouseUp = null;
  4558. }
  4559. if (params.moved) {
  4560. // fire a timechanged event
  4561. events.trigger(this, 'timechanged', {customTime: this.customTime});
  4562. }
  4563. };
  4564. /**
  4565. * An ItemSet holds a set of items and ranges which can be displayed in a
  4566. * range. The width is determined by the parent of the ItemSet, and the height
  4567. * is determined by the size of the items.
  4568. * @param {Component} parent
  4569. * @param {Component[]} [depends] Components on which this components depends
  4570. * (except for the parent)
  4571. * @param {Object} [options] See ItemSet.setOptions for the available
  4572. * options.
  4573. * @constructor ItemSet
  4574. * @extends Panel
  4575. */
  4576. // TODO: improve performance by replacing all Array.forEach with a for loop
  4577. function ItemSet(parent, depends, options) {
  4578. this.id = util.randomUUID();
  4579. this.parent = parent;
  4580. this.depends = depends;
  4581. // one options object is shared by this itemset and all its items
  4582. this.options = options || {};
  4583. this.defaultOptions = {
  4584. type: 'box',
  4585. align: 'center',
  4586. orientation: 'bottom',
  4587. margin: {
  4588. axis: 20,
  4589. item: 10
  4590. },
  4591. padding: 5
  4592. };
  4593. this.dom = {};
  4594. var me = this;
  4595. this.itemsData = null; // DataSet
  4596. this.range = null; // Range or Object {start: number, end: number}
  4597. this.listeners = {
  4598. 'add': function (event, params, senderId) {
  4599. if (senderId != me.id) {
  4600. me._onAdd(params.items);
  4601. }
  4602. },
  4603. 'update': function (event, params, senderId) {
  4604. if (senderId != me.id) {
  4605. me._onUpdate(params.items);
  4606. }
  4607. },
  4608. 'remove': function (event, params, senderId) {
  4609. if (senderId != me.id) {
  4610. me._onRemove(params.items);
  4611. }
  4612. }
  4613. };
  4614. this.items = {}; // object with an Item for every data item
  4615. this.selection = []; // list with the ids of all selected nodes
  4616. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4617. this.stack = new Stack(this, Object.create(this.options));
  4618. this.conversion = null;
  4619. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4620. }
  4621. ItemSet.prototype = new Panel();
  4622. // available item types will be registered here
  4623. ItemSet.types = {
  4624. box: ItemBox,
  4625. range: ItemRange,
  4626. rangeoverflow: ItemRangeOverflow,
  4627. point: ItemPoint
  4628. };
  4629. /**
  4630. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4631. * @param {Object} [options] The following options are available:
  4632. * {String | function} [className]
  4633. * class name for the itemset
  4634. * {String} [type]
  4635. * Default type for the items. Choose from 'box'
  4636. * (default), 'point', or 'range'. The default
  4637. * Style can be overwritten by individual items.
  4638. * {String} align
  4639. * Alignment for the items, only applicable for
  4640. * ItemBox. Choose 'center' (default), 'left', or
  4641. * 'right'.
  4642. * {String} orientation
  4643. * Orientation of the item set. Choose 'top' or
  4644. * 'bottom' (default).
  4645. * {Number} margin.axis
  4646. * Margin between the axis and the items in pixels.
  4647. * Default is 20.
  4648. * {Number} margin.item
  4649. * Margin between items in pixels. Default is 10.
  4650. * {Number} padding
  4651. * Padding of the contents of an item in pixels.
  4652. * Must correspond with the items css. Default is 5.
  4653. */
  4654. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4655. /**
  4656. * Set range (start and end).
  4657. * @param {Range | Object} range A Range or an object containing start and end.
  4658. */
  4659. ItemSet.prototype.setRange = function setRange(range) {
  4660. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4661. throw new TypeError('Range must be an instance of Range, ' +
  4662. 'or an object containing start and end.');
  4663. }
  4664. this.range = range;
  4665. };
  4666. /**
  4667. * Set selected items by their id. Replaces the current selection
  4668. * Unknown id's are silently ignored.
  4669. * @param {Array} [ids] An array with zero or more id's of the items to be
  4670. * selected. If ids is an empty array, all items will be
  4671. * unselected.
  4672. */
  4673. ItemSet.prototype.setSelection = function setSelection(ids) {
  4674. var i, ii, id, item, selection;
  4675. if (ids) {
  4676. if (!Array.isArray(ids)) {
  4677. throw new TypeError('Array expected');
  4678. }
  4679. // unselect currently selected items
  4680. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4681. id = this.selection[i];
  4682. item = this.items[id];
  4683. if (item) item.unselect();
  4684. }
  4685. // select items
  4686. this.selection = [];
  4687. for (i = 0, ii = ids.length; i < ii; i++) {
  4688. id = ids[i];
  4689. item = this.items[id];
  4690. if (item) {
  4691. this.selection.push(id);
  4692. item.select();
  4693. }
  4694. }
  4695. // trigger a select event
  4696. selection = this.selection.concat([]);
  4697. events.trigger(this, 'select', {
  4698. ids: selection
  4699. });
  4700. if (this.controller) {
  4701. this.requestRepaint();
  4702. }
  4703. }
  4704. };
  4705. /**
  4706. * Get the selected items by their id
  4707. * @return {Array} ids The ids of the selected items
  4708. */
  4709. ItemSet.prototype.getSelection = function getSelection() {
  4710. return this.selection.concat([]);
  4711. };
  4712. /**
  4713. * Deselect a selected item
  4714. * @param {String | Number} id
  4715. * @private
  4716. */
  4717. ItemSet.prototype._deselect = function _deselect(id) {
  4718. var selection = this.selection;
  4719. for (var i = 0, ii = selection.length; i < ii; i++) {
  4720. if (selection[i] == id) { // non-strict comparison!
  4721. selection.splice(i, 1);
  4722. break;
  4723. }
  4724. }
  4725. };
  4726. /**
  4727. * Repaint the component
  4728. * @return {Boolean} changed
  4729. */
  4730. ItemSet.prototype.repaint = function repaint() {
  4731. var changed = 0,
  4732. update = util.updateProperty,
  4733. asSize = util.option.asSize,
  4734. options = this.options,
  4735. orientation = this.getOption('orientation'),
  4736. defaultOptions = this.defaultOptions,
  4737. frame = this.frame;
  4738. if (!frame) {
  4739. frame = document.createElement('div');
  4740. frame.className = 'itemset';
  4741. var className = options.className;
  4742. if (className) {
  4743. util.addClassName(frame, util.option.asString(className));
  4744. }
  4745. // create background panel
  4746. var background = document.createElement('div');
  4747. background.className = 'background';
  4748. frame.appendChild(background);
  4749. this.dom.background = background;
  4750. // create foreground panel
  4751. var foreground = document.createElement('div');
  4752. foreground.className = 'foreground';
  4753. frame.appendChild(foreground);
  4754. this.dom.foreground = foreground;
  4755. // create axis panel
  4756. var axis = document.createElement('div');
  4757. axis.className = 'itemset-axis';
  4758. //frame.appendChild(axis);
  4759. this.dom.axis = axis;
  4760. this.frame = frame;
  4761. changed += 1;
  4762. }
  4763. if (!this.parent) {
  4764. throw new Error('Cannot repaint itemset: no parent attached');
  4765. }
  4766. var parentContainer = this.parent.getContainer();
  4767. if (!parentContainer) {
  4768. throw new Error('Cannot repaint itemset: parent has no container element');
  4769. }
  4770. if (!frame.parentNode) {
  4771. parentContainer.appendChild(frame);
  4772. changed += 1;
  4773. }
  4774. if (!this.dom.axis.parentNode) {
  4775. parentContainer.appendChild(this.dom.axis);
  4776. changed += 1;
  4777. }
  4778. // reposition frame
  4779. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4780. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4781. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4782. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4783. // reposition axis
  4784. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4785. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4786. if (orientation == 'bottom') {
  4787. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4788. }
  4789. else { // orientation == 'top'
  4790. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4791. }
  4792. this._updateConversion();
  4793. var me = this,
  4794. queue = this.queue,
  4795. itemsData = this.itemsData,
  4796. items = this.items,
  4797. dataOptions = {
  4798. // TODO: cleanup
  4799. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  4800. };
  4801. // show/hide added/changed/removed items
  4802. for (var id in queue) {
  4803. if (queue.hasOwnProperty(id)) {
  4804. var entry = queue[id],
  4805. item = items[id],
  4806. action = entry.action;
  4807. //noinspection FallthroughInSwitchStatementJS
  4808. switch (action) {
  4809. case 'add':
  4810. case 'update':
  4811. var itemData = itemsData && itemsData.get(id, dataOptions);
  4812. if (itemData) {
  4813. var type = itemData.type ||
  4814. (itemData.start && itemData.end && 'range') ||
  4815. options.type ||
  4816. 'box';
  4817. var constructor = ItemSet.types[type];
  4818. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4819. if (item) {
  4820. // update item
  4821. if (!constructor || !(item instanceof constructor)) {
  4822. // item type has changed, hide and delete the item
  4823. changed += item.hide();
  4824. item = null;
  4825. }
  4826. else {
  4827. item.data = itemData; // TODO: create a method item.setData ?
  4828. changed++;
  4829. }
  4830. }
  4831. if (!item) {
  4832. // create item
  4833. if (constructor) {
  4834. item = new constructor(me, itemData, options, defaultOptions);
  4835. item.id = entry.id; // we take entry.id, as id itself is stringified
  4836. changed++;
  4837. }
  4838. else {
  4839. throw new TypeError('Unknown item type "' + type + '"');
  4840. }
  4841. }
  4842. // force a repaint (not only a reposition)
  4843. item.repaint();
  4844. items[id] = item;
  4845. }
  4846. // update queue
  4847. delete queue[id];
  4848. break;
  4849. case 'remove':
  4850. if (item) {
  4851. // remove the item from the set selected items
  4852. if (item.selected) {
  4853. me._deselect(id);
  4854. }
  4855. // remove DOM of the item
  4856. changed += item.hide();
  4857. }
  4858. // update lists
  4859. delete items[id];
  4860. delete queue[id];
  4861. break;
  4862. default:
  4863. console.log('Error: unknown action "' + action + '"');
  4864. }
  4865. }
  4866. }
  4867. // reposition all items. Show items only when in the visible area
  4868. util.forEach(this.items, function (item) {
  4869. if (item.visible) {
  4870. changed += item.show();
  4871. item.reposition();
  4872. }
  4873. else {
  4874. changed += item.hide();
  4875. }
  4876. });
  4877. return (changed > 0);
  4878. };
  4879. /**
  4880. * Get the foreground container element
  4881. * @return {HTMLElement} foreground
  4882. */
  4883. ItemSet.prototype.getForeground = function getForeground() {
  4884. return this.dom.foreground;
  4885. };
  4886. /**
  4887. * Get the background container element
  4888. * @return {HTMLElement} background
  4889. */
  4890. ItemSet.prototype.getBackground = function getBackground() {
  4891. return this.dom.background;
  4892. };
  4893. /**
  4894. * Get the axis container element
  4895. * @return {HTMLElement} axis
  4896. */
  4897. ItemSet.prototype.getAxis = function getAxis() {
  4898. return this.dom.axis;
  4899. };
  4900. /**
  4901. * Reflow the component
  4902. * @return {Boolean} resized
  4903. */
  4904. ItemSet.prototype.reflow = function reflow () {
  4905. var changed = 0,
  4906. options = this.options,
  4907. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4908. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4909. update = util.updateProperty,
  4910. asNumber = util.option.asNumber,
  4911. asSize = util.option.asSize,
  4912. frame = this.frame;
  4913. if (frame) {
  4914. this._updateConversion();
  4915. util.forEach(this.items, function (item) {
  4916. changed += item.reflow();
  4917. });
  4918. // TODO: stack.update should be triggered via an event, in stack itself
  4919. // TODO: only update the stack when there are changed items
  4920. this.stack.update();
  4921. var maxHeight = asNumber(options.maxHeight);
  4922. var fixedHeight = (asSize(options.height) != null);
  4923. var height;
  4924. if (fixedHeight) {
  4925. height = frame.offsetHeight;
  4926. }
  4927. else {
  4928. // height is not specified, determine the height from the height and positioned items
  4929. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4930. if (visibleItems.length) {
  4931. var min = visibleItems[0].top;
  4932. var max = visibleItems[0].top + visibleItems[0].height;
  4933. util.forEach(visibleItems, function (item) {
  4934. min = Math.min(min, item.top);
  4935. max = Math.max(max, (item.top + item.height));
  4936. });
  4937. height = (max - min) + marginAxis + marginItem;
  4938. }
  4939. else {
  4940. height = marginAxis + marginItem;
  4941. }
  4942. }
  4943. if (maxHeight != null) {
  4944. height = Math.min(height, maxHeight);
  4945. }
  4946. changed += update(this, 'height', height);
  4947. // calculate height from items
  4948. changed += update(this, 'top', frame.offsetTop);
  4949. changed += update(this, 'left', frame.offsetLeft);
  4950. changed += update(this, 'width', frame.offsetWidth);
  4951. }
  4952. else {
  4953. changed += 1;
  4954. }
  4955. return (changed > 0);
  4956. };
  4957. /**
  4958. * Hide this component from the DOM
  4959. * @return {Boolean} changed
  4960. */
  4961. ItemSet.prototype.hide = function hide() {
  4962. var changed = false;
  4963. // remove the DOM
  4964. if (this.frame && this.frame.parentNode) {
  4965. this.frame.parentNode.removeChild(this.frame);
  4966. changed = true;
  4967. }
  4968. if (this.dom.axis && this.dom.axis.parentNode) {
  4969. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4970. changed = true;
  4971. }
  4972. return changed;
  4973. };
  4974. /**
  4975. * Set items
  4976. * @param {vis.DataSet | null} items
  4977. */
  4978. ItemSet.prototype.setItems = function setItems(items) {
  4979. var me = this,
  4980. ids,
  4981. oldItemsData = this.itemsData;
  4982. // replace the dataset
  4983. if (!items) {
  4984. this.itemsData = null;
  4985. }
  4986. else if (items instanceof DataSet || items instanceof DataView) {
  4987. this.itemsData = items;
  4988. }
  4989. else {
  4990. throw new TypeError('Data must be an instance of DataSet');
  4991. }
  4992. if (oldItemsData) {
  4993. // unsubscribe from old dataset
  4994. util.forEach(this.listeners, function (callback, event) {
  4995. oldItemsData.unsubscribe(event, callback);
  4996. });
  4997. // remove all drawn items
  4998. ids = oldItemsData.getIds();
  4999. this._onRemove(ids);
  5000. }
  5001. if (this.itemsData) {
  5002. // subscribe to new dataset
  5003. var id = this.id;
  5004. util.forEach(this.listeners, function (callback, event) {
  5005. me.itemsData.subscribe(event, callback, id);
  5006. });
  5007. // draw all new items
  5008. ids = this.itemsData.getIds();
  5009. this._onAdd(ids);
  5010. }
  5011. };
  5012. /**
  5013. * Get the current items items
  5014. * @returns {vis.DataSet | null}
  5015. */
  5016. ItemSet.prototype.getItems = function getItems() {
  5017. return this.itemsData;
  5018. };
  5019. /**
  5020. * Handle updated items
  5021. * @param {Number[]} ids
  5022. * @private
  5023. */
  5024. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  5025. this._toQueue('update', ids);
  5026. };
  5027. /**
  5028. * Handle changed items
  5029. * @param {Number[]} ids
  5030. * @private
  5031. */
  5032. ItemSet.prototype._onAdd = function _onAdd(ids) {
  5033. this._toQueue('add', ids);
  5034. };
  5035. /**
  5036. * Handle removed items
  5037. * @param {Number[]} ids
  5038. * @private
  5039. */
  5040. ItemSet.prototype._onRemove = function _onRemove(ids) {
  5041. this._toQueue('remove', ids);
  5042. };
  5043. /**
  5044. * Put items in the queue to be added/updated/remove
  5045. * @param {String} action can be 'add', 'update', 'remove'
  5046. * @param {Number[]} ids
  5047. */
  5048. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  5049. var queue = this.queue;
  5050. ids.forEach(function (id) {
  5051. queue[id] = {
  5052. id: id,
  5053. action: action
  5054. };
  5055. });
  5056. if (this.controller) {
  5057. //this.requestReflow();
  5058. this.requestRepaint();
  5059. }
  5060. };
  5061. /**
  5062. * Calculate the scale and offset to convert a position on screen to the
  5063. * corresponding date and vice versa.
  5064. * After the method _updateConversion is executed once, the methods toTime
  5065. * and toScreen can be used.
  5066. * @private
  5067. */
  5068. ItemSet.prototype._updateConversion = function _updateConversion() {
  5069. var range = this.range;
  5070. if (!range) {
  5071. throw new Error('No range configured');
  5072. }
  5073. if (range.conversion) {
  5074. this.conversion = range.conversion(this.width);
  5075. }
  5076. else {
  5077. this.conversion = Range.conversion(range.start, range.end, this.width);
  5078. }
  5079. };
  5080. /**
  5081. * Convert a position on screen (pixels) to a datetime
  5082. * Before this method can be used, the method _updateConversion must be
  5083. * executed once.
  5084. * @param {int} x Position on the screen in pixels
  5085. * @return {Date} time The datetime the corresponds with given position x
  5086. */
  5087. ItemSet.prototype.toTime = function toTime(x) {
  5088. var conversion = this.conversion;
  5089. return new Date(x / conversion.scale + conversion.offset);
  5090. };
  5091. /**
  5092. * Convert a datetime (Date object) into a position on the screen
  5093. * Before this method can be used, the method _updateConversion must be
  5094. * executed once.
  5095. * @param {Date} time A date
  5096. * @return {int} x The position on the screen in pixels which corresponds
  5097. * with the given date.
  5098. */
  5099. ItemSet.prototype.toScreen = function toScreen(time) {
  5100. var conversion = this.conversion;
  5101. return (time.valueOf() - conversion.offset) * conversion.scale;
  5102. };
  5103. /**
  5104. * @constructor Item
  5105. * @param {ItemSet} parent
  5106. * @param {Object} data Object containing (optional) parameters type,
  5107. * start, end, content, group, className.
  5108. * @param {Object} [options] Options to set initial property values
  5109. * @param {Object} [defaultOptions] default options
  5110. * // TODO: describe available options
  5111. */
  5112. function Item (parent, data, options, defaultOptions) {
  5113. this.parent = parent;
  5114. this.data = data;
  5115. this.dom = null;
  5116. this.options = options || {};
  5117. this.defaultOptions = defaultOptions || {};
  5118. this.selected = false;
  5119. this.visible = false;
  5120. this.top = 0;
  5121. this.left = 0;
  5122. this.width = 0;
  5123. this.height = 0;
  5124. }
  5125. /**
  5126. * Select current item
  5127. */
  5128. Item.prototype.select = function select() {
  5129. this.selected = true;
  5130. if (this.visible) this.repaint();
  5131. };
  5132. /**
  5133. * Unselect current item
  5134. */
  5135. Item.prototype.unselect = function unselect() {
  5136. this.selected = false;
  5137. if (this.visible) this.repaint();
  5138. };
  5139. /**
  5140. * Show the Item in the DOM (when not already visible)
  5141. * @return {Boolean} changed
  5142. */
  5143. Item.prototype.show = function show() {
  5144. return false;
  5145. };
  5146. /**
  5147. * Hide the Item from the DOM (when visible)
  5148. * @return {Boolean} changed
  5149. */
  5150. Item.prototype.hide = function hide() {
  5151. return false;
  5152. };
  5153. /**
  5154. * Repaint the item
  5155. * @return {Boolean} changed
  5156. */
  5157. Item.prototype.repaint = function repaint() {
  5158. // should be implemented by the item
  5159. return false;
  5160. };
  5161. /**
  5162. * Reflow the item
  5163. * @return {Boolean} resized
  5164. */
  5165. Item.prototype.reflow = function reflow() {
  5166. // should be implemented by the item
  5167. return false;
  5168. };
  5169. /**
  5170. * Return the items width
  5171. * @return {Integer} width
  5172. */
  5173. Item.prototype.getWidth = function getWidth() {
  5174. return this.width;
  5175. }
  5176. /**
  5177. * @constructor ItemBox
  5178. * @extends Item
  5179. * @param {ItemSet} parent
  5180. * @param {Object} data Object containing parameters start
  5181. * content, className.
  5182. * @param {Object} [options] Options to set initial property values
  5183. * @param {Object} [defaultOptions] default options
  5184. * // TODO: describe available options
  5185. */
  5186. function ItemBox (parent, data, options, defaultOptions) {
  5187. this.props = {
  5188. dot: {
  5189. left: 0,
  5190. top: 0,
  5191. width: 0,
  5192. height: 0
  5193. },
  5194. line: {
  5195. top: 0,
  5196. left: 0,
  5197. width: 0,
  5198. height: 0
  5199. }
  5200. };
  5201. Item.call(this, parent, data, options, defaultOptions);
  5202. }
  5203. ItemBox.prototype = new Item (null, null);
  5204. /**
  5205. * Repaint the item
  5206. * @return {Boolean} changed
  5207. */
  5208. ItemBox.prototype.repaint = function repaint() {
  5209. // TODO: make an efficient repaint
  5210. var changed = false;
  5211. var dom = this.dom;
  5212. if (!dom) {
  5213. this._create();
  5214. dom = this.dom;
  5215. changed = true;
  5216. }
  5217. if (dom) {
  5218. if (!this.parent) {
  5219. throw new Error('Cannot repaint item: no parent attached');
  5220. }
  5221. if (!dom.box.parentNode) {
  5222. var foreground = this.parent.getForeground();
  5223. if (!foreground) {
  5224. throw new Error('Cannot repaint time axis: ' +
  5225. 'parent has no foreground container element');
  5226. }
  5227. foreground.appendChild(dom.box);
  5228. changed = true;
  5229. }
  5230. if (!dom.line.parentNode) {
  5231. var background = this.parent.getBackground();
  5232. if (!background) {
  5233. throw new Error('Cannot repaint time axis: ' +
  5234. 'parent has no background container element');
  5235. }
  5236. background.appendChild(dom.line);
  5237. changed = true;
  5238. }
  5239. if (!dom.dot.parentNode) {
  5240. var axis = this.parent.getAxis();
  5241. if (!background) {
  5242. throw new Error('Cannot repaint time axis: ' +
  5243. 'parent has no axis container element');
  5244. }
  5245. axis.appendChild(dom.dot);
  5246. changed = true;
  5247. }
  5248. // update contents
  5249. if (this.data.content != this.content) {
  5250. this.content = this.data.content;
  5251. if (this.content instanceof Element) {
  5252. dom.content.innerHTML = '';
  5253. dom.content.appendChild(this.content);
  5254. }
  5255. else if (this.data.content != undefined) {
  5256. dom.content.innerHTML = this.content;
  5257. }
  5258. else {
  5259. throw new Error('Property "content" missing in item ' + this.data.id);
  5260. }
  5261. changed = true;
  5262. }
  5263. // update class
  5264. var className = (this.data.className? ' ' + this.data.className : '') +
  5265. (this.selected ? ' selected' : '');
  5266. if (this.className != className) {
  5267. this.className = className;
  5268. dom.box.className = 'item box' + className;
  5269. dom.line.className = 'item line' + className;
  5270. dom.dot.className = 'item dot' + className;
  5271. changed = true;
  5272. }
  5273. }
  5274. return changed;
  5275. };
  5276. /**
  5277. * Show the item in the DOM (when not already visible). The items DOM will
  5278. * be created when needed.
  5279. * @return {Boolean} changed
  5280. */
  5281. ItemBox.prototype.show = function show() {
  5282. if (!this.dom || !this.dom.box.parentNode) {
  5283. return this.repaint();
  5284. }
  5285. else {
  5286. return false;
  5287. }
  5288. };
  5289. /**
  5290. * Hide the item from the DOM (when visible)
  5291. * @return {Boolean} changed
  5292. */
  5293. ItemBox.prototype.hide = function hide() {
  5294. var changed = false,
  5295. dom = this.dom;
  5296. if (dom) {
  5297. if (dom.box.parentNode) {
  5298. dom.box.parentNode.removeChild(dom.box);
  5299. changed = true;
  5300. }
  5301. if (dom.line.parentNode) {
  5302. dom.line.parentNode.removeChild(dom.line);
  5303. }
  5304. if (dom.dot.parentNode) {
  5305. dom.dot.parentNode.removeChild(dom.dot);
  5306. }
  5307. }
  5308. return changed;
  5309. };
  5310. /**
  5311. * Reflow the item: calculate its actual size and position from the DOM
  5312. * @return {boolean} resized returns true if the axis is resized
  5313. * @override
  5314. */
  5315. ItemBox.prototype.reflow = function reflow() {
  5316. var changed = 0,
  5317. update,
  5318. dom,
  5319. props,
  5320. options,
  5321. margin,
  5322. start,
  5323. align,
  5324. orientation,
  5325. top,
  5326. left,
  5327. data,
  5328. range;
  5329. if (this.data.start == undefined) {
  5330. throw new Error('Property "start" missing in item ' + this.data.id);
  5331. }
  5332. data = this.data;
  5333. range = this.parent && this.parent.range;
  5334. if (data && range) {
  5335. // TODO: account for the width of the item
  5336. var interval = (range.end - range.start);
  5337. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  5338. }
  5339. else {
  5340. this.visible = false;
  5341. }
  5342. if (this.visible) {
  5343. dom = this.dom;
  5344. if (dom) {
  5345. update = util.updateProperty;
  5346. props = this.props;
  5347. options = this.options;
  5348. start = this.parent.toScreen(this.data.start);
  5349. align = options.align || this.defaultOptions.align;
  5350. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5351. orientation = options.orientation || this.defaultOptions.orientation;
  5352. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5353. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5354. changed += update(props.line, 'width', dom.line.offsetWidth);
  5355. changed += update(props.line, 'height', dom.line.offsetHeight);
  5356. changed += update(props.line, 'top', dom.line.offsetTop);
  5357. changed += update(this, 'width', dom.box.offsetWidth);
  5358. changed += update(this, 'height', dom.box.offsetHeight);
  5359. if (align == 'right') {
  5360. left = start - this.width;
  5361. }
  5362. else if (align == 'left') {
  5363. left = start;
  5364. }
  5365. else {
  5366. // default or 'center'
  5367. left = start - this.width / 2;
  5368. }
  5369. changed += update(this, 'left', left);
  5370. changed += update(props.line, 'left', start - props.line.width / 2);
  5371. changed += update(props.dot, 'left', start - props.dot.width / 2);
  5372. changed += update(props.dot, 'top', -props.dot.height / 2);
  5373. if (orientation == 'top') {
  5374. top = margin;
  5375. changed += update(this, 'top', top);
  5376. }
  5377. else {
  5378. // default or 'bottom'
  5379. var parentHeight = this.parent.height;
  5380. top = parentHeight - this.height - margin;
  5381. changed += update(this, 'top', top);
  5382. }
  5383. }
  5384. else {
  5385. changed += 1;
  5386. }
  5387. }
  5388. return (changed > 0);
  5389. };
  5390. /**
  5391. * Create an items DOM
  5392. * @private
  5393. */
  5394. ItemBox.prototype._create = function _create() {
  5395. var dom = this.dom;
  5396. if (!dom) {
  5397. this.dom = dom = {};
  5398. // create the box
  5399. dom.box = document.createElement('DIV');
  5400. // className is updated in repaint()
  5401. // contents box (inside the background box). used for making margins
  5402. dom.content = document.createElement('DIV');
  5403. dom.content.className = 'content';
  5404. dom.box.appendChild(dom.content);
  5405. // line to axis
  5406. dom.line = document.createElement('DIV');
  5407. dom.line.className = 'line';
  5408. // dot on axis
  5409. dom.dot = document.createElement('DIV');
  5410. dom.dot.className = 'dot';
  5411. // attach this item as attribute
  5412. dom.box['timeline-item'] = this;
  5413. }
  5414. };
  5415. /**
  5416. * Reposition the item, recalculate its left, top, and width, using the current
  5417. * range and size of the items itemset
  5418. * @override
  5419. */
  5420. ItemBox.prototype.reposition = function reposition() {
  5421. var dom = this.dom,
  5422. props = this.props,
  5423. orientation = this.options.orientation || this.defaultOptions.orientation;
  5424. if (dom) {
  5425. var box = dom.box,
  5426. line = dom.line,
  5427. dot = dom.dot;
  5428. box.style.left = this.left + 'px';
  5429. box.style.top = this.top + 'px';
  5430. line.style.left = props.line.left + 'px';
  5431. if (orientation == 'top') {
  5432. line.style.top = 0 + 'px';
  5433. line.style.height = this.top + 'px';
  5434. }
  5435. else {
  5436. // orientation 'bottom'
  5437. line.style.top = (this.top + this.height) + 'px';
  5438. line.style.height = Math.max(this.parent.height - this.top - this.height +
  5439. this.props.dot.height / 2, 0) + 'px';
  5440. }
  5441. dot.style.left = props.dot.left + 'px';
  5442. dot.style.top = props.dot.top + 'px';
  5443. }
  5444. };
  5445. /**
  5446. * @constructor ItemPoint
  5447. * @extends Item
  5448. * @param {ItemSet} parent
  5449. * @param {Object} data Object containing parameters start
  5450. * content, className.
  5451. * @param {Object} [options] Options to set initial property values
  5452. * @param {Object} [defaultOptions] default options
  5453. * // TODO: describe available options
  5454. */
  5455. function ItemPoint (parent, data, options, defaultOptions) {
  5456. this.props = {
  5457. dot: {
  5458. top: 0,
  5459. width: 0,
  5460. height: 0
  5461. },
  5462. content: {
  5463. height: 0,
  5464. marginLeft: 0
  5465. }
  5466. };
  5467. Item.call(this, parent, data, options, defaultOptions);
  5468. }
  5469. ItemPoint.prototype = new Item (null, null);
  5470. /**
  5471. * Repaint the item
  5472. * @return {Boolean} changed
  5473. */
  5474. ItemPoint.prototype.repaint = function repaint() {
  5475. // TODO: make an efficient repaint
  5476. var changed = false;
  5477. var dom = this.dom;
  5478. if (!dom) {
  5479. this._create();
  5480. dom = this.dom;
  5481. changed = true;
  5482. }
  5483. if (dom) {
  5484. if (!this.parent) {
  5485. throw new Error('Cannot repaint item: no parent attached');
  5486. }
  5487. var foreground = this.parent.getForeground();
  5488. if (!foreground) {
  5489. throw new Error('Cannot repaint time axis: ' +
  5490. 'parent has no foreground container element');
  5491. }
  5492. if (!dom.point.parentNode) {
  5493. foreground.appendChild(dom.point);
  5494. foreground.appendChild(dom.point);
  5495. changed = true;
  5496. }
  5497. // update contents
  5498. if (this.data.content != this.content) {
  5499. this.content = this.data.content;
  5500. if (this.content instanceof Element) {
  5501. dom.content.innerHTML = '';
  5502. dom.content.appendChild(this.content);
  5503. }
  5504. else if (this.data.content != undefined) {
  5505. dom.content.innerHTML = this.content;
  5506. }
  5507. else {
  5508. throw new Error('Property "content" missing in item ' + this.data.id);
  5509. }
  5510. changed = true;
  5511. }
  5512. // update class
  5513. var className = (this.data.className? ' ' + this.data.className : '') +
  5514. (this.selected ? ' selected' : '');
  5515. if (this.className != className) {
  5516. this.className = className;
  5517. dom.point.className = 'item point' + className;
  5518. changed = true;
  5519. }
  5520. }
  5521. return changed;
  5522. };
  5523. /**
  5524. * Show the item in the DOM (when not already visible). The items DOM will
  5525. * be created when needed.
  5526. * @return {Boolean} changed
  5527. */
  5528. ItemPoint.prototype.show = function show() {
  5529. if (!this.dom || !this.dom.point.parentNode) {
  5530. return this.repaint();
  5531. }
  5532. else {
  5533. return false;
  5534. }
  5535. };
  5536. /**
  5537. * Hide the item from the DOM (when visible)
  5538. * @return {Boolean} changed
  5539. */
  5540. ItemPoint.prototype.hide = function hide() {
  5541. var changed = false,
  5542. dom = this.dom;
  5543. if (dom) {
  5544. if (dom.point.parentNode) {
  5545. dom.point.parentNode.removeChild(dom.point);
  5546. changed = true;
  5547. }
  5548. }
  5549. return changed;
  5550. };
  5551. /**
  5552. * Reflow the item: calculate its actual size from the DOM
  5553. * @return {boolean} resized returns true if the axis is resized
  5554. * @override
  5555. */
  5556. ItemPoint.prototype.reflow = function reflow() {
  5557. var changed = 0,
  5558. update,
  5559. dom,
  5560. props,
  5561. options,
  5562. margin,
  5563. orientation,
  5564. start,
  5565. top,
  5566. data,
  5567. range;
  5568. if (this.data.start == undefined) {
  5569. throw new Error('Property "start" missing in item ' + this.data.id);
  5570. }
  5571. data = this.data;
  5572. range = this.parent && this.parent.range;
  5573. if (data && range) {
  5574. // TODO: account for the width of the item
  5575. var interval = (range.end - range.start);
  5576. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  5577. }
  5578. else {
  5579. this.visible = false;
  5580. }
  5581. if (this.visible) {
  5582. dom = this.dom;
  5583. if (dom) {
  5584. update = util.updateProperty;
  5585. props = this.props;
  5586. options = this.options;
  5587. orientation = options.orientation || this.defaultOptions.orientation;
  5588. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5589. start = this.parent.toScreen(this.data.start);
  5590. changed += update(this, 'width', dom.point.offsetWidth);
  5591. changed += update(this, 'height', dom.point.offsetHeight);
  5592. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5593. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5594. changed += update(props.content, 'height', dom.content.offsetHeight);
  5595. if (orientation == 'top') {
  5596. top = margin;
  5597. }
  5598. else {
  5599. // default or 'bottom'
  5600. var parentHeight = this.parent.height;
  5601. top = Math.max(parentHeight - this.height - margin, 0);
  5602. }
  5603. changed += update(this, 'top', top);
  5604. changed += update(this, 'left', start - props.dot.width / 2);
  5605. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5606. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5607. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5608. }
  5609. else {
  5610. changed += 1;
  5611. }
  5612. }
  5613. return (changed > 0);
  5614. };
  5615. /**
  5616. * Create an items DOM
  5617. * @private
  5618. */
  5619. ItemPoint.prototype._create = function _create() {
  5620. var dom = this.dom;
  5621. if (!dom) {
  5622. this.dom = dom = {};
  5623. // background box
  5624. dom.point = document.createElement('div');
  5625. // className is updated in repaint()
  5626. // contents box, right from the dot
  5627. dom.content = document.createElement('div');
  5628. dom.content.className = 'content';
  5629. dom.point.appendChild(dom.content);
  5630. // dot at start
  5631. dom.dot = document.createElement('div');
  5632. dom.dot.className = 'dot';
  5633. dom.point.appendChild(dom.dot);
  5634. // attach this item as attribute
  5635. dom.point['timeline-item'] = this;
  5636. }
  5637. };
  5638. /**
  5639. * Reposition the item, recalculate its left, top, and width, using the current
  5640. * range and size of the items itemset
  5641. * @override
  5642. */
  5643. ItemPoint.prototype.reposition = function reposition() {
  5644. var dom = this.dom,
  5645. props = this.props;
  5646. if (dom) {
  5647. dom.point.style.top = this.top + 'px';
  5648. dom.point.style.left = this.left + 'px';
  5649. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5650. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5651. dom.dot.style.top = props.dot.top + 'px';
  5652. }
  5653. };
  5654. /**
  5655. * @constructor ItemRange
  5656. * @extends Item
  5657. * @param {ItemSet} parent
  5658. * @param {Object} data Object containing parameters start, end
  5659. * content, className.
  5660. * @param {Object} [options] Options to set initial property values
  5661. * @param {Object} [defaultOptions] default options
  5662. * // TODO: describe available options
  5663. */
  5664. function ItemRange (parent, data, options, defaultOptions) {
  5665. this.props = {
  5666. content: {
  5667. left: 0,
  5668. width: 0
  5669. }
  5670. };
  5671. Item.call(this, parent, data, options, defaultOptions);
  5672. }
  5673. ItemRange.prototype = new Item (null, null);
  5674. /**
  5675. * Repaint the item
  5676. * @return {Boolean} changed
  5677. */
  5678. ItemRange.prototype.repaint = function repaint() {
  5679. // TODO: make an efficient repaint
  5680. var changed = false;
  5681. var dom = this.dom;
  5682. if (!dom) {
  5683. this._create();
  5684. dom = this.dom;
  5685. changed = true;
  5686. }
  5687. if (dom) {
  5688. if (!this.parent) {
  5689. throw new Error('Cannot repaint item: no parent attached');
  5690. }
  5691. var foreground = this.parent.getForeground();
  5692. if (!foreground) {
  5693. throw new Error('Cannot repaint time axis: ' +
  5694. 'parent has no foreground container element');
  5695. }
  5696. if (!dom.box.parentNode) {
  5697. foreground.appendChild(dom.box);
  5698. changed = true;
  5699. }
  5700. // update content
  5701. if (this.data.content != this.content) {
  5702. this.content = this.data.content;
  5703. if (this.content instanceof Element) {
  5704. dom.content.innerHTML = '';
  5705. dom.content.appendChild(this.content);
  5706. }
  5707. else if (this.data.content != undefined) {
  5708. dom.content.innerHTML = this.content;
  5709. }
  5710. else {
  5711. throw new Error('Property "content" missing in item ' + this.data.id);
  5712. }
  5713. changed = true;
  5714. }
  5715. // update class
  5716. var className = (this.data.className? ' ' + this.data.className : '') +
  5717. (this.selected ? ' selected' : '');
  5718. if (this.className != className) {
  5719. this.className = className;
  5720. dom.box.className = 'item range' + className;
  5721. changed = true;
  5722. }
  5723. }
  5724. return changed;
  5725. };
  5726. /**
  5727. * Show the item in the DOM (when not already visible). The items DOM will
  5728. * be created when needed.
  5729. * @return {Boolean} changed
  5730. */
  5731. ItemRange.prototype.show = function show() {
  5732. if (!this.dom || !this.dom.box.parentNode) {
  5733. return this.repaint();
  5734. }
  5735. else {
  5736. return false;
  5737. }
  5738. };
  5739. /**
  5740. * Hide the item from the DOM (when visible)
  5741. * @return {Boolean} changed
  5742. */
  5743. ItemRange.prototype.hide = function hide() {
  5744. var changed = false,
  5745. dom = this.dom;
  5746. if (dom) {
  5747. if (dom.box.parentNode) {
  5748. dom.box.parentNode.removeChild(dom.box);
  5749. changed = true;
  5750. }
  5751. }
  5752. return changed;
  5753. };
  5754. /**
  5755. * Reflow the item: calculate its actual size from the DOM
  5756. * @return {boolean} resized returns true if the axis is resized
  5757. * @override
  5758. */
  5759. ItemRange.prototype.reflow = function reflow() {
  5760. var changed = 0,
  5761. dom,
  5762. props,
  5763. options,
  5764. margin,
  5765. padding,
  5766. parent,
  5767. start,
  5768. end,
  5769. data,
  5770. range,
  5771. update,
  5772. box,
  5773. parentWidth,
  5774. contentLeft,
  5775. orientation,
  5776. top;
  5777. if (this.data.start == undefined) {
  5778. throw new Error('Property "start" missing in item ' + this.data.id);
  5779. }
  5780. if (this.data.end == undefined) {
  5781. throw new Error('Property "end" missing in item ' + this.data.id);
  5782. }
  5783. data = this.data;
  5784. range = this.parent && this.parent.range;
  5785. if (data && range) {
  5786. // TODO: account for the width of the item. Take some margin
  5787. this.visible = (data.start < range.end) && (data.end > range.start);
  5788. }
  5789. else {
  5790. this.visible = false;
  5791. }
  5792. if (this.visible) {
  5793. dom = this.dom;
  5794. if (dom) {
  5795. props = this.props;
  5796. options = this.options;
  5797. parent = this.parent;
  5798. start = parent.toScreen(this.data.start);
  5799. end = parent.toScreen(this.data.end);
  5800. update = util.updateProperty;
  5801. box = dom.box;
  5802. parentWidth = parent.width;
  5803. orientation = options.orientation || this.defaultOptions.orientation;
  5804. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5805. padding = options.padding || this.defaultOptions.padding;
  5806. changed += update(props.content, 'width', dom.content.offsetWidth);
  5807. changed += update(this, 'height', box.offsetHeight);
  5808. // limit the width of the this, as browsers cannot draw very wide divs
  5809. if (start < -parentWidth) {
  5810. start = -parentWidth;
  5811. }
  5812. if (end > 2 * parentWidth) {
  5813. end = 2 * parentWidth;
  5814. }
  5815. // when range exceeds left of the window, position the contents at the left of the visible area
  5816. if (start < 0) {
  5817. contentLeft = Math.min(-start,
  5818. (end - start - props.content.width - 2 * padding));
  5819. // TODO: remove the need for options.padding. it's terrible.
  5820. }
  5821. else {
  5822. contentLeft = 0;
  5823. }
  5824. changed += update(props.content, 'left', contentLeft);
  5825. if (orientation == 'top') {
  5826. top = margin;
  5827. changed += update(this, 'top', top);
  5828. }
  5829. else {
  5830. // default or 'bottom'
  5831. top = parent.height - this.height - margin;
  5832. changed += update(this, 'top', top);
  5833. }
  5834. changed += update(this, 'left', start);
  5835. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5836. }
  5837. else {
  5838. changed += 1;
  5839. }
  5840. }
  5841. return (changed > 0);
  5842. };
  5843. /**
  5844. * Create an items DOM
  5845. * @private
  5846. */
  5847. ItemRange.prototype._create = function _create() {
  5848. var dom = this.dom;
  5849. if (!dom) {
  5850. this.dom = dom = {};
  5851. // background box
  5852. dom.box = document.createElement('div');
  5853. // className is updated in repaint()
  5854. // contents box
  5855. dom.content = document.createElement('div');
  5856. dom.content.className = 'content';
  5857. dom.box.appendChild(dom.content);
  5858. // attach this item as attribute
  5859. dom.box['timeline-item'] = this;
  5860. }
  5861. };
  5862. /**
  5863. * Reposition the item, recalculate its left, top, and width, using the current
  5864. * range and size of the items itemset
  5865. * @override
  5866. */
  5867. ItemRange.prototype.reposition = function reposition() {
  5868. var dom = this.dom,
  5869. props = this.props;
  5870. if (dom) {
  5871. dom.box.style.top = this.top + 'px';
  5872. dom.box.style.left = this.left + 'px';
  5873. dom.box.style.width = this.width + 'px';
  5874. dom.content.style.left = props.content.left + 'px';
  5875. }
  5876. };
  5877. /**
  5878. * @constructor ItemRangeOverflow
  5879. * @extends ItemRange
  5880. * @param {ItemSet} parent
  5881. * @param {Object} data Object containing parameters start, end
  5882. * content, className.
  5883. * @param {Object} [options] Options to set initial property values
  5884. * @param {Object} [defaultOptions] default options
  5885. * // TODO: describe available options
  5886. */
  5887. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  5888. this.props = {
  5889. content: {
  5890. left: 0,
  5891. width: 0
  5892. }
  5893. };
  5894. ItemRange.call(this, parent, data, options, defaultOptions);
  5895. }
  5896. ItemRangeOverflow.prototype = new ItemRange (null, null);
  5897. /**
  5898. * Repaint the item
  5899. * @return {Boolean} changed
  5900. */
  5901. ItemRangeOverflow.prototype.repaint = function repaint() {
  5902. // TODO: make an efficient repaint
  5903. var changed = false;
  5904. var dom = this.dom;
  5905. if (!dom) {
  5906. this._create();
  5907. dom = this.dom;
  5908. changed = true;
  5909. }
  5910. if (dom) {
  5911. if (!this.parent) {
  5912. throw new Error('Cannot repaint item: no parent attached');
  5913. }
  5914. var foreground = this.parent.getForeground();
  5915. if (!foreground) {
  5916. throw new Error('Cannot repaint time axis: ' +
  5917. 'parent has no foreground container element');
  5918. }
  5919. if (!dom.box.parentNode) {
  5920. foreground.appendChild(dom.box);
  5921. changed = true;
  5922. }
  5923. // update content
  5924. if (this.data.content != this.content) {
  5925. this.content = this.data.content;
  5926. if (this.content instanceof Element) {
  5927. dom.content.innerHTML = '';
  5928. dom.content.appendChild(this.content);
  5929. }
  5930. else if (this.data.content != undefined) {
  5931. dom.content.innerHTML = this.content;
  5932. }
  5933. else {
  5934. throw new Error('Property "content" missing in item ' + this.data.id);
  5935. }
  5936. changed = true;
  5937. }
  5938. // update class
  5939. var className = this.data.className ? (' ' + this.data.className) : '';
  5940. if (this.className != className) {
  5941. this.className = className;
  5942. dom.box.className = 'item rangeoverflow' + className;
  5943. changed = true;
  5944. }
  5945. }
  5946. return changed;
  5947. };
  5948. /**
  5949. * Return the items width
  5950. * @return {Number} width
  5951. */
  5952. ItemRangeOverflow.prototype.getWidth = function getWidth() {
  5953. if (this.props.content !== undefined && this.width < this.props.content.width)
  5954. return this.props.content.width;
  5955. else
  5956. return this.width;
  5957. };
  5958. /**
  5959. * @constructor Group
  5960. * @param {GroupSet} parent
  5961. * @param {Number | String} groupId
  5962. * @param {Object} [options] Options to set initial property values
  5963. * // TODO: describe available options
  5964. * @extends Component
  5965. */
  5966. function Group (parent, groupId, options) {
  5967. this.id = util.randomUUID();
  5968. this.parent = parent;
  5969. this.groupId = groupId;
  5970. this.itemset = null; // ItemSet
  5971. this.options = options || {};
  5972. this.options.top = 0;
  5973. this.props = {
  5974. label: {
  5975. width: 0,
  5976. height: 0
  5977. }
  5978. };
  5979. this.top = 0;
  5980. this.left = 0;
  5981. this.width = 0;
  5982. this.height = 0;
  5983. }
  5984. Group.prototype = new Component();
  5985. // TODO: comment
  5986. Group.prototype.setOptions = Component.prototype.setOptions;
  5987. /**
  5988. * Get the container element of the panel, which can be used by a child to
  5989. * add its own widgets.
  5990. * @returns {HTMLElement} container
  5991. */
  5992. Group.prototype.getContainer = function () {
  5993. return this.parent.getContainer();
  5994. };
  5995. /**
  5996. * Set item set for the group. The group will create a view on the itemset,
  5997. * filtered by the groups id.
  5998. * @param {DataSet | DataView} items
  5999. */
  6000. Group.prototype.setItems = function setItems(items) {
  6001. if (this.itemset) {
  6002. // remove current item set
  6003. this.itemset.hide();
  6004. this.itemset.setItems();
  6005. this.parent.controller.remove(this.itemset);
  6006. this.itemset = null;
  6007. }
  6008. if (items) {
  6009. var groupId = this.groupId;
  6010. var itemsetOptions = Object.create(this.options);
  6011. this.itemset = new ItemSet(this, null, itemsetOptions);
  6012. this.itemset.setRange(this.parent.range);
  6013. this.view = new DataView(items, {
  6014. filter: function (item) {
  6015. return item.group == groupId;
  6016. }
  6017. });
  6018. this.itemset.setItems(this.view);
  6019. this.parent.controller.add(this.itemset);
  6020. }
  6021. };
  6022. /**
  6023. * Set selected items by their id. Replaces the current selection.
  6024. * Unknown id's are silently ignored.
  6025. * @param {Array} [ids] An array with zero or more id's of the items to be
  6026. * selected. If ids is an empty array, all items will be
  6027. * unselected.
  6028. */
  6029. Group.prototype.setSelection = function setSelection(ids) {
  6030. if (this.itemset) this.itemset.setSelection(ids);
  6031. };
  6032. /**
  6033. * Get the selected items by their id
  6034. * @return {Array} ids The ids of the selected items
  6035. */
  6036. Group.prototype.getSelection = function getSelection() {
  6037. return this.itemset ? this.itemset.getSelection() : [];
  6038. };
  6039. /**
  6040. * Repaint the item
  6041. * @return {Boolean} changed
  6042. */
  6043. Group.prototype.repaint = function repaint() {
  6044. return false;
  6045. };
  6046. /**
  6047. * Reflow the item
  6048. * @return {Boolean} resized
  6049. */
  6050. Group.prototype.reflow = function reflow() {
  6051. var changed = 0,
  6052. update = util.updateProperty;
  6053. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  6054. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  6055. // TODO: reckon with the height of the group label
  6056. if (this.label) {
  6057. var inner = this.label.firstChild;
  6058. changed += update(this.props.label, 'width', inner.clientWidth);
  6059. changed += update(this.props.label, 'height', inner.clientHeight);
  6060. }
  6061. else {
  6062. changed += update(this.props.label, 'width', 0);
  6063. changed += update(this.props.label, 'height', 0);
  6064. }
  6065. return (changed > 0);
  6066. };
  6067. /**
  6068. * An GroupSet holds a set of groups
  6069. * @param {Component} parent
  6070. * @param {Component[]} [depends] Components on which this components depends
  6071. * (except for the parent)
  6072. * @param {Object} [options] See GroupSet.setOptions for the available
  6073. * options.
  6074. * @constructor GroupSet
  6075. * @extends Panel
  6076. */
  6077. function GroupSet(parent, depends, options) {
  6078. this.id = util.randomUUID();
  6079. this.parent = parent;
  6080. this.depends = depends;
  6081. this.options = options || {};
  6082. this.range = null; // Range or Object {start: number, end: number}
  6083. this.itemsData = null; // DataSet with items
  6084. this.groupsData = null; // DataSet with groups
  6085. this.groups = {}; // map with groups
  6086. this.dom = {};
  6087. this.props = {
  6088. labels: {
  6089. width: 0
  6090. }
  6091. };
  6092. // TODO: implement right orientation of the labels
  6093. // changes in groups are queued key/value map containing id/action
  6094. this.queue = {};
  6095. var me = this;
  6096. this.listeners = {
  6097. 'add': function (event, params) {
  6098. me._onAdd(params.items);
  6099. },
  6100. 'update': function (event, params) {
  6101. me._onUpdate(params.items);
  6102. },
  6103. 'remove': function (event, params) {
  6104. me._onRemove(params.items);
  6105. }
  6106. };
  6107. }
  6108. GroupSet.prototype = new Panel();
  6109. /**
  6110. * Set options for the GroupSet. Existing options will be extended/overwritten.
  6111. * @param {Object} [options] The following options are available:
  6112. * {String | function} groupsOrder
  6113. * TODO: describe options
  6114. */
  6115. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  6116. GroupSet.prototype.setRange = function (range) {
  6117. // TODO: implement setRange
  6118. };
  6119. /**
  6120. * Set items
  6121. * @param {vis.DataSet | null} items
  6122. */
  6123. GroupSet.prototype.setItems = function setItems(items) {
  6124. this.itemsData = items;
  6125. for (var id in this.groups) {
  6126. if (this.groups.hasOwnProperty(id)) {
  6127. var group = this.groups[id];
  6128. group.setItems(items);
  6129. }
  6130. }
  6131. };
  6132. /**
  6133. * Get items
  6134. * @return {vis.DataSet | null} items
  6135. */
  6136. GroupSet.prototype.getItems = function getItems() {
  6137. return this.itemsData;
  6138. };
  6139. /**
  6140. * Set range (start and end).
  6141. * @param {Range | Object} range A Range or an object containing start and end.
  6142. */
  6143. GroupSet.prototype.setRange = function setRange(range) {
  6144. this.range = range;
  6145. };
  6146. /**
  6147. * Set groups
  6148. * @param {vis.DataSet} groups
  6149. */
  6150. GroupSet.prototype.setGroups = function setGroups(groups) {
  6151. var me = this,
  6152. ids;
  6153. // unsubscribe from current dataset
  6154. if (this.groupsData) {
  6155. util.forEach(this.listeners, function (callback, event) {
  6156. me.groupsData.unsubscribe(event, callback);
  6157. });
  6158. // remove all drawn groups
  6159. ids = this.groupsData.getIds();
  6160. this._onRemove(ids);
  6161. }
  6162. // replace the dataset
  6163. if (!groups) {
  6164. this.groupsData = null;
  6165. }
  6166. else if (groups instanceof DataSet) {
  6167. this.groupsData = groups;
  6168. }
  6169. else {
  6170. this.groupsData = new DataSet({
  6171. convert: {
  6172. start: 'Date',
  6173. end: 'Date'
  6174. }
  6175. });
  6176. this.groupsData.add(groups);
  6177. }
  6178. if (this.groupsData) {
  6179. // subscribe to new dataset
  6180. var id = this.id;
  6181. util.forEach(this.listeners, function (callback, event) {
  6182. me.groupsData.subscribe(event, callback, id);
  6183. });
  6184. // draw all new groups
  6185. ids = this.groupsData.getIds();
  6186. this._onAdd(ids);
  6187. }
  6188. };
  6189. /**
  6190. * Get groups
  6191. * @return {vis.DataSet | null} groups
  6192. */
  6193. GroupSet.prototype.getGroups = function getGroups() {
  6194. return this.groupsData;
  6195. };
  6196. /**
  6197. * Set selected items by their id. Replaces the current selection.
  6198. * Unknown id's are silently ignored.
  6199. * @param {Array} [ids] An array with zero or more id's of the items to be
  6200. * selected. If ids is an empty array, all items will be
  6201. * unselected.
  6202. */
  6203. GroupSet.prototype.setSelection = function setSelection(ids) {
  6204. var selection = [],
  6205. groups = this.groups;
  6206. // iterate over each of the groups
  6207. for (var id in groups) {
  6208. if (groups.hasOwnProperty(id)) {
  6209. var group = groups[id];
  6210. group.setSelection(ids);
  6211. }
  6212. }
  6213. return selection;
  6214. };
  6215. /**
  6216. * Get the selected items by their id
  6217. * @return {Array} ids The ids of the selected items
  6218. */
  6219. GroupSet.prototype.getSelection = function getSelection() {
  6220. var selection = [],
  6221. groups = this.groups;
  6222. // iterate over each of the groups
  6223. for (var id in groups) {
  6224. if (groups.hasOwnProperty(id)) {
  6225. var group = groups[id];
  6226. selection = selection.concat(group.getSelection());
  6227. }
  6228. }
  6229. return selection;
  6230. };
  6231. /**
  6232. * Repaint the component
  6233. * @return {Boolean} changed
  6234. */
  6235. GroupSet.prototype.repaint = function repaint() {
  6236. var changed = 0,
  6237. i, id, group, label,
  6238. update = util.updateProperty,
  6239. asSize = util.option.asSize,
  6240. asElement = util.option.asElement,
  6241. options = this.options,
  6242. frame = this.dom.frame,
  6243. labels = this.dom.labels,
  6244. labelSet = this.dom.labelSet;
  6245. // create frame
  6246. if (!this.parent) {
  6247. throw new Error('Cannot repaint groupset: no parent attached');
  6248. }
  6249. var parentContainer = this.parent.getContainer();
  6250. if (!parentContainer) {
  6251. throw new Error('Cannot repaint groupset: parent has no container element');
  6252. }
  6253. if (!frame) {
  6254. frame = document.createElement('div');
  6255. frame.className = 'groupset';
  6256. this.dom.frame = frame;
  6257. var className = options.className;
  6258. if (className) {
  6259. util.addClassName(frame, util.option.asString(className));
  6260. }
  6261. changed += 1;
  6262. }
  6263. if (!frame.parentNode) {
  6264. parentContainer.appendChild(frame);
  6265. changed += 1;
  6266. }
  6267. // create labels
  6268. var labelContainer = asElement(options.labelContainer);
  6269. if (!labelContainer) {
  6270. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  6271. }
  6272. if (!labels) {
  6273. labels = document.createElement('div');
  6274. labels.className = 'labels';
  6275. this.dom.labels = labels;
  6276. }
  6277. if (!labelSet) {
  6278. labelSet = document.createElement('div');
  6279. labelSet.className = 'label-set';
  6280. labels.appendChild(labelSet);
  6281. this.dom.labelSet = labelSet;
  6282. }
  6283. if (!labels.parentNode || labels.parentNode != labelContainer) {
  6284. if (labels.parentNode) {
  6285. labels.parentNode.removeChild(labels.parentNode);
  6286. }
  6287. labelContainer.appendChild(labels);
  6288. }
  6289. // reposition frame
  6290. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  6291. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6292. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6293. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6294. // reposition labels
  6295. changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
  6296. changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
  6297. var me = this,
  6298. queue = this.queue,
  6299. groups = this.groups,
  6300. groupsData = this.groupsData;
  6301. // show/hide added/changed/removed groups
  6302. var ids = Object.keys(queue);
  6303. if (ids.length) {
  6304. ids.forEach(function (id) {
  6305. var action = queue[id];
  6306. var group = groups[id];
  6307. //noinspection FallthroughInSwitchStatementJS
  6308. switch (action) {
  6309. case 'add':
  6310. case 'update':
  6311. if (!group) {
  6312. var groupOptions = Object.create(me.options);
  6313. util.extend(groupOptions, {
  6314. height: null,
  6315. maxHeight: null
  6316. });
  6317. group = new Group(me, id, groupOptions);
  6318. group.setItems(me.itemsData); // attach items data
  6319. groups[id] = group;
  6320. me.controller.add(group);
  6321. }
  6322. // TODO: update group data
  6323. group.data = groupsData.get(id);
  6324. delete queue[id];
  6325. break;
  6326. case 'remove':
  6327. if (group) {
  6328. group.setItems(); // detach items data
  6329. delete groups[id];
  6330. me.controller.remove(group);
  6331. }
  6332. // update lists
  6333. delete queue[id];
  6334. break;
  6335. default:
  6336. console.log('Error: unknown action "' + action + '"');
  6337. }
  6338. });
  6339. // the groupset depends on each of the groups
  6340. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  6341. // TODO: apply dependencies of the groupset
  6342. // update the top positions of the groups in the correct order
  6343. var orderedGroups = this.groupsData.getIds({
  6344. order: this.options.groupOrder
  6345. });
  6346. for (i = 0; i < orderedGroups.length; i++) {
  6347. (function (group, prevGroup) {
  6348. var top = 0;
  6349. if (prevGroup) {
  6350. top = function () {
  6351. // TODO: top must reckon with options.maxHeight
  6352. return prevGroup.top + prevGroup.height;
  6353. }
  6354. }
  6355. group.setOptions({
  6356. top: top
  6357. });
  6358. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  6359. }
  6360. // (re)create the labels
  6361. while (labelSet.firstChild) {
  6362. labelSet.removeChild(labelSet.firstChild);
  6363. }
  6364. for (i = 0; i < orderedGroups.length; i++) {
  6365. id = orderedGroups[i];
  6366. label = this._createLabel(id);
  6367. labelSet.appendChild(label);
  6368. }
  6369. changed++;
  6370. }
  6371. // reposition the labels
  6372. // TODO: labels are not displayed correctly when orientation=='top'
  6373. // TODO: width of labelPanel is not immediately updated on a change in groups
  6374. for (id in groups) {
  6375. if (groups.hasOwnProperty(id)) {
  6376. group = groups[id];
  6377. label = group.label;
  6378. if (label) {
  6379. label.style.top = group.top + 'px';
  6380. label.style.height = group.height + 'px';
  6381. }
  6382. }
  6383. }
  6384. return (changed > 0);
  6385. };
  6386. /**
  6387. * Create a label for group with given id
  6388. * @param {Number} id
  6389. * @return {Element} label
  6390. * @private
  6391. */
  6392. GroupSet.prototype._createLabel = function(id) {
  6393. var group = this.groups[id];
  6394. var label = document.createElement('div');
  6395. label.className = 'label';
  6396. var inner = document.createElement('div');
  6397. inner.className = 'inner';
  6398. label.appendChild(inner);
  6399. var content = group.data && group.data.content;
  6400. if (content instanceof Element) {
  6401. inner.appendChild(content);
  6402. }
  6403. else if (content != undefined) {
  6404. inner.innerHTML = content;
  6405. }
  6406. var className = group.data && group.data.className;
  6407. if (className) {
  6408. util.addClassName(label, className);
  6409. }
  6410. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  6411. return label;
  6412. };
  6413. /**
  6414. * Get container element
  6415. * @return {HTMLElement} container
  6416. */
  6417. GroupSet.prototype.getContainer = function getContainer() {
  6418. return this.dom.frame;
  6419. };
  6420. /**
  6421. * Get the width of the group labels
  6422. * @return {Number} width
  6423. */
  6424. GroupSet.prototype.getLabelsWidth = function getContainer() {
  6425. return this.props.labels.width;
  6426. };
  6427. /**
  6428. * Reflow the component
  6429. * @return {Boolean} resized
  6430. */
  6431. GroupSet.prototype.reflow = function reflow() {
  6432. var changed = 0,
  6433. id, group,
  6434. options = this.options,
  6435. update = util.updateProperty,
  6436. asNumber = util.option.asNumber,
  6437. asSize = util.option.asSize,
  6438. frame = this.dom.frame;
  6439. if (frame) {
  6440. var maxHeight = asNumber(options.maxHeight);
  6441. var fixedHeight = (asSize(options.height) != null);
  6442. var height;
  6443. if (fixedHeight) {
  6444. height = frame.offsetHeight;
  6445. }
  6446. else {
  6447. // height is not specified, calculate the sum of the height of all groups
  6448. height = 0;
  6449. for (id in this.groups) {
  6450. if (this.groups.hasOwnProperty(id)) {
  6451. group = this.groups[id];
  6452. height += group.height;
  6453. }
  6454. }
  6455. }
  6456. if (maxHeight != null) {
  6457. height = Math.min(height, maxHeight);
  6458. }
  6459. changed += update(this, 'height', height);
  6460. changed += update(this, 'top', frame.offsetTop);
  6461. changed += update(this, 'left', frame.offsetLeft);
  6462. changed += update(this, 'width', frame.offsetWidth);
  6463. }
  6464. // calculate the maximum width of the labels
  6465. var width = 0;
  6466. for (id in this.groups) {
  6467. if (this.groups.hasOwnProperty(id)) {
  6468. group = this.groups[id];
  6469. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  6470. width = Math.max(width, labelWidth);
  6471. }
  6472. }
  6473. changed += update(this.props.labels, 'width', width);
  6474. return (changed > 0);
  6475. };
  6476. /**
  6477. * Hide the component from the DOM
  6478. * @return {Boolean} changed
  6479. */
  6480. GroupSet.prototype.hide = function hide() {
  6481. if (this.dom.frame && this.dom.frame.parentNode) {
  6482. this.dom.frame.parentNode.removeChild(this.dom.frame);
  6483. return true;
  6484. }
  6485. else {
  6486. return false;
  6487. }
  6488. };
  6489. /**
  6490. * Show the component in the DOM (when not already visible).
  6491. * A repaint will be executed when the component is not visible
  6492. * @return {Boolean} changed
  6493. */
  6494. GroupSet.prototype.show = function show() {
  6495. if (!this.dom.frame || !this.dom.frame.parentNode) {
  6496. return this.repaint();
  6497. }
  6498. else {
  6499. return false;
  6500. }
  6501. };
  6502. /**
  6503. * Handle updated groups
  6504. * @param {Number[]} ids
  6505. * @private
  6506. */
  6507. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  6508. this._toQueue(ids, 'update');
  6509. };
  6510. /**
  6511. * Handle changed groups
  6512. * @param {Number[]} ids
  6513. * @private
  6514. */
  6515. GroupSet.prototype._onAdd = function _onAdd(ids) {
  6516. this._toQueue(ids, 'add');
  6517. };
  6518. /**
  6519. * Handle removed groups
  6520. * @param {Number[]} ids
  6521. * @private
  6522. */
  6523. GroupSet.prototype._onRemove = function _onRemove(ids) {
  6524. this._toQueue(ids, 'remove');
  6525. };
  6526. /**
  6527. * Put groups in the queue to be added/updated/remove
  6528. * @param {Number[]} ids
  6529. * @param {String} action can be 'add', 'update', 'remove'
  6530. */
  6531. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  6532. var queue = this.queue;
  6533. ids.forEach(function (id) {
  6534. queue[id] = action;
  6535. });
  6536. if (this.controller) {
  6537. //this.requestReflow();
  6538. this.requestRepaint();
  6539. }
  6540. };
  6541. /**
  6542. * Create a timeline visualization
  6543. * @param {HTMLElement} container
  6544. * @param {vis.DataSet | Array | DataTable} [items]
  6545. * @param {Object} [options] See Timeline.setOptions for the available options.
  6546. * @constructor
  6547. */
  6548. function Timeline (container, items, options) {
  6549. var me = this;
  6550. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6551. this.options = {
  6552. orientation: 'bottom',
  6553. min: null,
  6554. max: null,
  6555. zoomMin: 10, // milliseconds
  6556. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6557. // moveable: true, // TODO: option moveable
  6558. // zoomable: true, // TODO: option zoomable
  6559. showMinorLabels: true,
  6560. showMajorLabels: true,
  6561. showCurrentTime: false,
  6562. showCustomTime: false,
  6563. autoResize: false
  6564. };
  6565. // controller
  6566. this.controller = new Controller();
  6567. // root panel
  6568. if (!container) {
  6569. throw new Error('No container element provided');
  6570. }
  6571. var rootOptions = Object.create(this.options);
  6572. rootOptions.height = function () {
  6573. // TODO: change to height
  6574. if (me.options.height) {
  6575. // fixed height
  6576. return me.options.height;
  6577. }
  6578. else {
  6579. // auto height
  6580. return (me.timeaxis.height + me.content.height) + 'px';
  6581. }
  6582. };
  6583. this.rootPanel = new RootPanel(container, rootOptions);
  6584. this.controller.add(this.rootPanel);
  6585. // item panel
  6586. var itemOptions = Object.create(this.options);
  6587. itemOptions.left = function () {
  6588. return me.labelPanel.width;
  6589. };
  6590. itemOptions.width = function () {
  6591. return me.rootPanel.width - me.labelPanel.width;
  6592. };
  6593. itemOptions.top = null;
  6594. itemOptions.height = null;
  6595. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  6596. this.controller.add(this.itemPanel);
  6597. // label panel
  6598. var labelOptions = Object.create(this.options);
  6599. labelOptions.top = null;
  6600. labelOptions.left = null;
  6601. labelOptions.height = null;
  6602. labelOptions.width = function () {
  6603. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  6604. return me.content.getLabelsWidth();
  6605. }
  6606. else {
  6607. return 0;
  6608. }
  6609. };
  6610. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  6611. this.controller.add(this.labelPanel);
  6612. // range
  6613. var rangeOptions = Object.create(this.options);
  6614. this.range = new Range(rangeOptions);
  6615. this.range.setRange(
  6616. now.clone().add('days', -3).valueOf(),
  6617. now.clone().add('days', 4).valueOf()
  6618. );
  6619. // TODO: reckon with options moveable and zoomable
  6620. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  6621. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  6622. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  6623. this.range.on('rangechange', function (properties) {
  6624. var force = true;
  6625. me.controller.requestReflow(force);
  6626. me._trigger('rangechange', properties);
  6627. });
  6628. this.range.on('rangechanged', function (properties) {
  6629. var force = true;
  6630. me.controller.requestReflow(force);
  6631. me._trigger('rangechanged', properties);
  6632. });
  6633. // single select (or unselect) when tapping an item
  6634. // TODO: implement ctrl+click
  6635. this.rootPanel.on('tap', this._onSelectItem.bind(this));
  6636. // multi select when holding mouse/touch, or on ctrl+click
  6637. this.rootPanel.on('hold', this._onMultiSelectItem.bind(this));
  6638. // time axis
  6639. var timeaxisOptions = Object.create(rootOptions);
  6640. timeaxisOptions.range = this.range;
  6641. timeaxisOptions.left = null;
  6642. timeaxisOptions.top = null;
  6643. timeaxisOptions.width = '100%';
  6644. timeaxisOptions.height = null;
  6645. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  6646. this.timeaxis.setRange(this.range);
  6647. this.controller.add(this.timeaxis);
  6648. // current time bar
  6649. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  6650. this.controller.add(this.currenttime);
  6651. // custom time bar
  6652. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  6653. this.controller.add(this.customtime);
  6654. // create groupset
  6655. this.setGroups(null);
  6656. this.itemsData = null; // DataSet
  6657. this.groupsData = null; // DataSet
  6658. // apply options
  6659. if (options) {
  6660. this.setOptions(options);
  6661. }
  6662. // create itemset and groupset
  6663. if (items) {
  6664. this.setItems(items);
  6665. }
  6666. }
  6667. /**
  6668. * Set options
  6669. * @param {Object} options TODO: describe the available options
  6670. */
  6671. Timeline.prototype.setOptions = function (options) {
  6672. util.extend(this.options, options);
  6673. // force update of range
  6674. // options.start and options.end can be undefined
  6675. //this.range.setRange(options.start, options.end);
  6676. this.range.setRange();
  6677. this.controller.reflow();
  6678. this.controller.repaint();
  6679. };
  6680. /**
  6681. * Set a custom time bar
  6682. * @param {Date} time
  6683. */
  6684. Timeline.prototype.setCustomTime = function (time) {
  6685. this.customtime._setCustomTime(time);
  6686. };
  6687. /**
  6688. * Retrieve the current custom time.
  6689. * @return {Date} customTime
  6690. */
  6691. Timeline.prototype.getCustomTime = function() {
  6692. return new Date(this.customtime.customTime.valueOf());
  6693. };
  6694. /**
  6695. * Set items
  6696. * @param {vis.DataSet | Array | DataTable | null} items
  6697. */
  6698. Timeline.prototype.setItems = function(items) {
  6699. var initialLoad = (this.itemsData == null);
  6700. // convert to type DataSet when needed
  6701. var newItemSet;
  6702. if (!items) {
  6703. newItemSet = null;
  6704. }
  6705. else if (items instanceof DataSet) {
  6706. newItemSet = items;
  6707. }
  6708. if (!(items instanceof DataSet)) {
  6709. newItemSet = new DataSet({
  6710. convert: {
  6711. start: 'Date',
  6712. end: 'Date'
  6713. }
  6714. });
  6715. newItemSet.add(items);
  6716. }
  6717. // set items
  6718. this.itemsData = newItemSet;
  6719. this.content.setItems(newItemSet);
  6720. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6721. // apply the data range as range
  6722. var dataRange = this.getItemRange();
  6723. // add 5% space on both sides
  6724. var min = dataRange.min;
  6725. var max = dataRange.max;
  6726. if (min != null && max != null) {
  6727. var interval = (max.valueOf() - min.valueOf());
  6728. if (interval <= 0) {
  6729. // prevent an empty interval
  6730. interval = 24 * 60 * 60 * 1000; // 1 day
  6731. }
  6732. min = new Date(min.valueOf() - interval * 0.05);
  6733. max = new Date(max.valueOf() + interval * 0.05);
  6734. }
  6735. // override specified start and/or end date
  6736. if (this.options.start != undefined) {
  6737. min = util.convert(this.options.start, 'Date');
  6738. }
  6739. if (this.options.end != undefined) {
  6740. max = util.convert(this.options.end, 'Date');
  6741. }
  6742. // apply range if there is a min or max available
  6743. if (min != null || max != null) {
  6744. this.range.setRange(min, max);
  6745. }
  6746. }
  6747. };
  6748. /**
  6749. * Set groups
  6750. * @param {vis.DataSet | Array | DataTable} groups
  6751. */
  6752. Timeline.prototype.setGroups = function(groups) {
  6753. var me = this;
  6754. this.groupsData = groups;
  6755. // switch content type between ItemSet or GroupSet when needed
  6756. var Type = this.groupsData ? GroupSet : ItemSet;
  6757. if (!(this.content instanceof Type)) {
  6758. // remove old content set
  6759. if (this.content) {
  6760. this.content.hide();
  6761. if (this.content.setItems) {
  6762. this.content.setItems(); // disconnect from items
  6763. }
  6764. if (this.content.setGroups) {
  6765. this.content.setGroups(); // disconnect from groups
  6766. }
  6767. this.controller.remove(this.content);
  6768. }
  6769. // create new content set
  6770. var options = Object.create(this.options);
  6771. util.extend(options, {
  6772. top: function () {
  6773. if (me.options.orientation == 'top') {
  6774. return me.timeaxis.height;
  6775. }
  6776. else {
  6777. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  6778. }
  6779. },
  6780. left: null,
  6781. width: '100%',
  6782. height: function () {
  6783. if (me.options.height) {
  6784. // fixed height
  6785. return me.itemPanel.height - me.timeaxis.height;
  6786. }
  6787. else {
  6788. // auto height
  6789. return null;
  6790. }
  6791. },
  6792. maxHeight: function () {
  6793. // TODO: change maxHeight to be a css string like '100%' or '300px'
  6794. if (me.options.maxHeight) {
  6795. if (!util.isNumber(me.options.maxHeight)) {
  6796. throw new TypeError('Number expected for property maxHeight');
  6797. }
  6798. return me.options.maxHeight - me.timeaxis.height;
  6799. }
  6800. else {
  6801. return null;
  6802. }
  6803. },
  6804. labelContainer: function () {
  6805. return me.labelPanel.getContainer();
  6806. }
  6807. });
  6808. this.content = new Type(this.itemPanel, [this.timeaxis], options);
  6809. if (this.content.setRange) {
  6810. this.content.setRange(this.range);
  6811. }
  6812. if (this.content.setItems) {
  6813. this.content.setItems(this.itemsData);
  6814. }
  6815. if (this.content.setGroups) {
  6816. this.content.setGroups(this.groupsData);
  6817. }
  6818. this.controller.add(this.content);
  6819. }
  6820. };
  6821. /**
  6822. * Get the data range of the item set.
  6823. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6824. * When no minimum is found, min==null
  6825. * When no maximum is found, max==null
  6826. */
  6827. Timeline.prototype.getItemRange = function getItemRange() {
  6828. // calculate min from start filed
  6829. var itemsData = this.itemsData,
  6830. min = null,
  6831. max = null;
  6832. if (itemsData) {
  6833. // calculate the minimum value of the field 'start'
  6834. var minItem = itemsData.min('start');
  6835. min = minItem ? minItem.start.valueOf() : null;
  6836. // calculate maximum value of fields 'start' and 'end'
  6837. var maxStartItem = itemsData.max('start');
  6838. if (maxStartItem) {
  6839. max = maxStartItem.start.valueOf();
  6840. }
  6841. var maxEndItem = itemsData.max('end');
  6842. if (maxEndItem) {
  6843. if (max == null) {
  6844. max = maxEndItem.end.valueOf();
  6845. }
  6846. else {
  6847. max = Math.max(max, maxEndItem.end.valueOf());
  6848. }
  6849. }
  6850. }
  6851. return {
  6852. min: (min != null) ? new Date(min) : null,
  6853. max: (max != null) ? new Date(max) : null
  6854. };
  6855. };
  6856. /**
  6857. * Set selected items by their id. Replaces the current selection
  6858. * Unknown id's are silently ignored.
  6859. * @param {Array} [ids] An array with zero or more id's of the items to be
  6860. * selected. If ids is an empty array, all items will be
  6861. * unselected.
  6862. */
  6863. Timeline.prototype.setSelection = function setSelection (ids) {
  6864. if (this.content) this.content.setSelection(ids);
  6865. };
  6866. /**
  6867. * Get the selected items by their id
  6868. * @return {Array} ids The ids of the selected items
  6869. */
  6870. Timeline.prototype.getSelection = function getSelection() {
  6871. return this.content ? this.content.getSelection() : [];
  6872. };
  6873. /**
  6874. * Add event listener
  6875. * @param {String} event Event name. Available events:
  6876. * 'rangechange', 'rangechanged', 'select'
  6877. * @param {function} callback Callback function, invoked as callback(properties)
  6878. * where properties is an optional object containing
  6879. * event specific properties.
  6880. */
  6881. Timeline.prototype.on = function on (event, callback) {
  6882. var available = ['rangechange', 'rangechanged', 'select'];
  6883. if (available.indexOf(event) == -1) {
  6884. throw new Error('Unknown event "' + event + '". Choose from ' + available.join());
  6885. }
  6886. events.addListener(this, event, callback);
  6887. };
  6888. /**
  6889. * Remove an event listener
  6890. * @param {String} event Event name
  6891. * @param {function} callback Callback function
  6892. */
  6893. Timeline.prototype.off = function off (event, callback) {
  6894. events.removeListener(this, event, callback);
  6895. };
  6896. /**
  6897. * Trigger an event
  6898. * @param {String} event Event name, available events: 'rangechange',
  6899. * 'rangechanged', 'select'
  6900. * @param {Object} [properties] Event specific properties
  6901. * @private
  6902. */
  6903. Timeline.prototype._trigger = function _trigger(event, properties) {
  6904. events.trigger(this, event, properties || {});
  6905. };
  6906. /**
  6907. * Handle selecting/deselecting an item when tapping it
  6908. * @param {Event} event
  6909. * @private
  6910. */
  6911. Timeline.prototype._onSelectItem = function (event) {
  6912. var item = this._itemFromTarget(event);
  6913. var selection = item ? [item.id] : [];
  6914. this.setSelection(selection);
  6915. this._trigger('select', {
  6916. items: this.getSelection()
  6917. });
  6918. event.stopPropagation();
  6919. };
  6920. /**
  6921. * Handle selecting/deselecting multiple items when holding an item
  6922. * @param {Event} event
  6923. * @private
  6924. */
  6925. Timeline.prototype._onMultiSelectItem = function (event) {
  6926. var selection,
  6927. item = this._itemFromTarget(event);
  6928. if (!item) {
  6929. // do nothing...
  6930. return;
  6931. }
  6932. selection = this.getSelection(); // current selection
  6933. var index = selection.indexOf(item.id);
  6934. if (index == -1) {
  6935. // item is not yet selected -> select it
  6936. selection.push(item.id);
  6937. }
  6938. else {
  6939. // item is already selected -> deselect it
  6940. selection.splice(index, 1);
  6941. }
  6942. this.setSelection(selection);
  6943. this._trigger('select', {
  6944. items: this.getSelection()
  6945. });
  6946. event.stopPropagation();
  6947. };
  6948. /**
  6949. * Find an item from an event target:
  6950. * searches for the attribute 'timeline-item' in the event target's element tree
  6951. * @param {Event} event
  6952. * @return {Item | null| item
  6953. * @private
  6954. */
  6955. Timeline.prototype._itemFromTarget = function _itemFromTarget (event) {
  6956. var target = event.target;
  6957. while (target) {
  6958. if (target.hasOwnProperty('timeline-item')) {
  6959. return target['timeline-item'];
  6960. }
  6961. target = target.parentNode;
  6962. }
  6963. return null;
  6964. };
  6965. (function(exports) {
  6966. /**
  6967. * Parse a text source containing data in DOT language into a JSON object.
  6968. * The object contains two lists: one with nodes and one with edges.
  6969. *
  6970. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6971. *
  6972. * @param {String} data Text containing a graph in DOT-notation
  6973. * @return {Object} graph An object containing two parameters:
  6974. * {Object[]} nodes
  6975. * {Object[]} edges
  6976. */
  6977. function parseDOT (data) {
  6978. dot = data;
  6979. return parseGraph();
  6980. }
  6981. // token types enumeration
  6982. var TOKENTYPE = {
  6983. NULL : 0,
  6984. DELIMITER : 1,
  6985. IDENTIFIER: 2,
  6986. UNKNOWN : 3
  6987. };
  6988. // map with all delimiters
  6989. var DELIMITERS = {
  6990. '{': true,
  6991. '}': true,
  6992. '[': true,
  6993. ']': true,
  6994. ';': true,
  6995. '=': true,
  6996. ',': true,
  6997. '->': true,
  6998. '--': true
  6999. };
  7000. var dot = ''; // current dot file
  7001. var index = 0; // current index in dot file
  7002. var c = ''; // current token character in expr
  7003. var token = ''; // current token
  7004. var tokenType = TOKENTYPE.NULL; // type of the token
  7005. /**
  7006. * Get the first character from the dot file.
  7007. * The character is stored into the char c. If the end of the dot file is
  7008. * reached, the function puts an empty string in c.
  7009. */
  7010. function first() {
  7011. index = 0;
  7012. c = dot.charAt(0);
  7013. }
  7014. /**
  7015. * Get the next character from the dot file.
  7016. * The character is stored into the char c. If the end of the dot file is
  7017. * reached, the function puts an empty string in c.
  7018. */
  7019. function next() {
  7020. index++;
  7021. c = dot.charAt(index);
  7022. }
  7023. /**
  7024. * Preview the next character from the dot file.
  7025. * @return {String} cNext
  7026. */
  7027. function nextPreview() {
  7028. return dot.charAt(index + 1);
  7029. }
  7030. /**
  7031. * Test whether given character is alphabetic or numeric
  7032. * @param {String} c
  7033. * @return {Boolean} isAlphaNumeric
  7034. */
  7035. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  7036. function isAlphaNumeric(c) {
  7037. return regexAlphaNumeric.test(c);
  7038. }
  7039. /**
  7040. * Merge all properties of object b into object b
  7041. * @param {Object} a
  7042. * @param {Object} b
  7043. * @return {Object} a
  7044. */
  7045. function merge (a, b) {
  7046. if (!a) {
  7047. a = {};
  7048. }
  7049. if (b) {
  7050. for (var name in b) {
  7051. if (b.hasOwnProperty(name)) {
  7052. a[name] = b[name];
  7053. }
  7054. }
  7055. }
  7056. return a;
  7057. }
  7058. /**
  7059. * Set a value in an object, where the provided parameter name can be a
  7060. * path with nested parameters. For example:
  7061. *
  7062. * var obj = {a: 2};
  7063. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  7064. *
  7065. * @param {Object} obj
  7066. * @param {String} path A parameter name or dot-separated parameter path,
  7067. * like "color.highlight.border".
  7068. * @param {*} value
  7069. */
  7070. function setValue(obj, path, value) {
  7071. var keys = path.split('.');
  7072. var o = obj;
  7073. while (keys.length) {
  7074. var key = keys.shift();
  7075. if (keys.length) {
  7076. // this isn't the end point
  7077. if (!o[key]) {
  7078. o[key] = {};
  7079. }
  7080. o = o[key];
  7081. }
  7082. else {
  7083. // this is the end point
  7084. o[key] = value;
  7085. }
  7086. }
  7087. }
  7088. /**
  7089. * Add a node to a graph object. If there is already a node with
  7090. * the same id, their attributes will be merged.
  7091. * @param {Object} graph
  7092. * @param {Object} node
  7093. */
  7094. function addNode(graph, node) {
  7095. var i, len;
  7096. var current = null;
  7097. // find root graph (in case of subgraph)
  7098. var graphs = [graph]; // list with all graphs from current graph to root graph
  7099. var root = graph;
  7100. while (root.parent) {
  7101. graphs.push(root.parent);
  7102. root = root.parent;
  7103. }
  7104. // find existing node (at root level) by its id
  7105. if (root.nodes) {
  7106. for (i = 0, len = root.nodes.length; i < len; i++) {
  7107. if (node.id === root.nodes[i].id) {
  7108. current = root.nodes[i];
  7109. break;
  7110. }
  7111. }
  7112. }
  7113. if (!current) {
  7114. // this is a new node
  7115. current = {
  7116. id: node.id
  7117. };
  7118. if (graph.node) {
  7119. // clone default attributes
  7120. current.attr = merge(current.attr, graph.node);
  7121. }
  7122. }
  7123. // add node to this (sub)graph and all its parent graphs
  7124. for (i = graphs.length - 1; i >= 0; i--) {
  7125. var g = graphs[i];
  7126. if (!g.nodes) {
  7127. g.nodes = [];
  7128. }
  7129. if (g.nodes.indexOf(current) == -1) {
  7130. g.nodes.push(current);
  7131. }
  7132. }
  7133. // merge attributes
  7134. if (node.attr) {
  7135. current.attr = merge(current.attr, node.attr);
  7136. }
  7137. }
  7138. /**
  7139. * Add an edge to a graph object
  7140. * @param {Object} graph
  7141. * @param {Object} edge
  7142. */
  7143. function addEdge(graph, edge) {
  7144. if (!graph.edges) {
  7145. graph.edges = [];
  7146. }
  7147. graph.edges.push(edge);
  7148. if (graph.edge) {
  7149. var attr = merge({}, graph.edge); // clone default attributes
  7150. edge.attr = merge(attr, edge.attr); // merge attributes
  7151. }
  7152. }
  7153. /**
  7154. * Create an edge to a graph object
  7155. * @param {Object} graph
  7156. * @param {String | Number | Object} from
  7157. * @param {String | Number | Object} to
  7158. * @param {String} type
  7159. * @param {Object | null} attr
  7160. * @return {Object} edge
  7161. */
  7162. function createEdge(graph, from, to, type, attr) {
  7163. var edge = {
  7164. from: from,
  7165. to: to,
  7166. type: type
  7167. };
  7168. if (graph.edge) {
  7169. edge.attr = merge({}, graph.edge); // clone default attributes
  7170. }
  7171. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  7172. return edge;
  7173. }
  7174. /**
  7175. * Get next token in the current dot file.
  7176. * The token and token type are available as token and tokenType
  7177. */
  7178. function getToken() {
  7179. tokenType = TOKENTYPE.NULL;
  7180. token = '';
  7181. // skip over whitespaces
  7182. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7183. next();
  7184. }
  7185. do {
  7186. var isComment = false;
  7187. // skip comment
  7188. if (c == '#') {
  7189. // find the previous non-space character
  7190. var i = index - 1;
  7191. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7192. i--;
  7193. }
  7194. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7195. // the # is at the start of a line, this is indeed a line comment
  7196. while (c != '' && c != '\n') {
  7197. next();
  7198. }
  7199. isComment = true;
  7200. }
  7201. }
  7202. if (c == '/' && nextPreview() == '/') {
  7203. // skip line comment
  7204. while (c != '' && c != '\n') {
  7205. next();
  7206. }
  7207. isComment = true;
  7208. }
  7209. if (c == '/' && nextPreview() == '*') {
  7210. // skip block comment
  7211. while (c != '') {
  7212. if (c == '*' && nextPreview() == '/') {
  7213. // end of block comment found. skip these last two characters
  7214. next();
  7215. next();
  7216. break;
  7217. }
  7218. else {
  7219. next();
  7220. }
  7221. }
  7222. isComment = true;
  7223. }
  7224. // skip over whitespaces
  7225. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7226. next();
  7227. }
  7228. }
  7229. while (isComment);
  7230. // check for end of dot file
  7231. if (c == '') {
  7232. // token is still empty
  7233. tokenType = TOKENTYPE.DELIMITER;
  7234. return;
  7235. }
  7236. // check for delimiters consisting of 2 characters
  7237. var c2 = c + nextPreview();
  7238. if (DELIMITERS[c2]) {
  7239. tokenType = TOKENTYPE.DELIMITER;
  7240. token = c2;
  7241. next();
  7242. next();
  7243. return;
  7244. }
  7245. // check for delimiters consisting of 1 character
  7246. if (DELIMITERS[c]) {
  7247. tokenType = TOKENTYPE.DELIMITER;
  7248. token = c;
  7249. next();
  7250. return;
  7251. }
  7252. // check for an identifier (number or string)
  7253. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7254. if (isAlphaNumeric(c) || c == '-') {
  7255. token += c;
  7256. next();
  7257. while (isAlphaNumeric(c)) {
  7258. token += c;
  7259. next();
  7260. }
  7261. if (token == 'false') {
  7262. token = false; // convert to boolean
  7263. }
  7264. else if (token == 'true') {
  7265. token = true; // convert to boolean
  7266. }
  7267. else if (!isNaN(Number(token))) {
  7268. token = Number(token); // convert to number
  7269. }
  7270. tokenType = TOKENTYPE.IDENTIFIER;
  7271. return;
  7272. }
  7273. // check for a string enclosed by double quotes
  7274. if (c == '"') {
  7275. next();
  7276. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7277. token += c;
  7278. if (c == '"') { // skip the escape character
  7279. next();
  7280. }
  7281. next();
  7282. }
  7283. if (c != '"') {
  7284. throw newSyntaxError('End of string " expected');
  7285. }
  7286. next();
  7287. tokenType = TOKENTYPE.IDENTIFIER;
  7288. return;
  7289. }
  7290. // something unknown is found, wrong characters, a syntax error
  7291. tokenType = TOKENTYPE.UNKNOWN;
  7292. while (c != '') {
  7293. token += c;
  7294. next();
  7295. }
  7296. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7297. }
  7298. /**
  7299. * Parse a graph.
  7300. * @returns {Object} graph
  7301. */
  7302. function parseGraph() {
  7303. var graph = {};
  7304. first();
  7305. getToken();
  7306. // optional strict keyword
  7307. if (token == 'strict') {
  7308. graph.strict = true;
  7309. getToken();
  7310. }
  7311. // graph or digraph keyword
  7312. if (token == 'graph' || token == 'digraph') {
  7313. graph.type = token;
  7314. getToken();
  7315. }
  7316. // optional graph id
  7317. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7318. graph.id = token;
  7319. getToken();
  7320. }
  7321. // open angle bracket
  7322. if (token != '{') {
  7323. throw newSyntaxError('Angle bracket { expected');
  7324. }
  7325. getToken();
  7326. // statements
  7327. parseStatements(graph);
  7328. // close angle bracket
  7329. if (token != '}') {
  7330. throw newSyntaxError('Angle bracket } expected');
  7331. }
  7332. getToken();
  7333. // end of file
  7334. if (token !== '') {
  7335. throw newSyntaxError('End of file expected');
  7336. }
  7337. getToken();
  7338. // remove temporary default properties
  7339. delete graph.node;
  7340. delete graph.edge;
  7341. delete graph.graph;
  7342. return graph;
  7343. }
  7344. /**
  7345. * Parse a list with statements.
  7346. * @param {Object} graph
  7347. */
  7348. function parseStatements (graph) {
  7349. while (token !== '' && token != '}') {
  7350. parseStatement(graph);
  7351. if (token == ';') {
  7352. getToken();
  7353. }
  7354. }
  7355. }
  7356. /**
  7357. * Parse a single statement. Can be a an attribute statement, node
  7358. * statement, a series of node statements and edge statements, or a
  7359. * parameter.
  7360. * @param {Object} graph
  7361. */
  7362. function parseStatement(graph) {
  7363. // parse subgraph
  7364. var subgraph = parseSubgraph(graph);
  7365. if (subgraph) {
  7366. // edge statements
  7367. parseEdge(graph, subgraph);
  7368. return;
  7369. }
  7370. // parse an attribute statement
  7371. var attr = parseAttributeStatement(graph);
  7372. if (attr) {
  7373. return;
  7374. }
  7375. // parse node
  7376. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7377. throw newSyntaxError('Identifier expected');
  7378. }
  7379. var id = token; // id can be a string or a number
  7380. getToken();
  7381. if (token == '=') {
  7382. // id statement
  7383. getToken();
  7384. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7385. throw newSyntaxError('Identifier expected');
  7386. }
  7387. graph[id] = token;
  7388. getToken();
  7389. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7390. }
  7391. else {
  7392. parseNodeStatement(graph, id);
  7393. }
  7394. }
  7395. /**
  7396. * Parse a subgraph
  7397. * @param {Object} graph parent graph object
  7398. * @return {Object | null} subgraph
  7399. */
  7400. function parseSubgraph (graph) {
  7401. var subgraph = null;
  7402. // optional subgraph keyword
  7403. if (token == 'subgraph') {
  7404. subgraph = {};
  7405. subgraph.type = 'subgraph';
  7406. getToken();
  7407. // optional graph id
  7408. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7409. subgraph.id = token;
  7410. getToken();
  7411. }
  7412. }
  7413. // open angle bracket
  7414. if (token == '{') {
  7415. getToken();
  7416. if (!subgraph) {
  7417. subgraph = {};
  7418. }
  7419. subgraph.parent = graph;
  7420. subgraph.node = graph.node;
  7421. subgraph.edge = graph.edge;
  7422. subgraph.graph = graph.graph;
  7423. // statements
  7424. parseStatements(subgraph);
  7425. // close angle bracket
  7426. if (token != '}') {
  7427. throw newSyntaxError('Angle bracket } expected');
  7428. }
  7429. getToken();
  7430. // remove temporary default properties
  7431. delete subgraph.node;
  7432. delete subgraph.edge;
  7433. delete subgraph.graph;
  7434. delete subgraph.parent;
  7435. // register at the parent graph
  7436. if (!graph.subgraphs) {
  7437. graph.subgraphs = [];
  7438. }
  7439. graph.subgraphs.push(subgraph);
  7440. }
  7441. return subgraph;
  7442. }
  7443. /**
  7444. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7445. * Available keywords are 'node', 'edge', 'graph'.
  7446. * The previous list with default attributes will be replaced
  7447. * @param {Object} graph
  7448. * @returns {String | null} keyword Returns the name of the parsed attribute
  7449. * (node, edge, graph), or null if nothing
  7450. * is parsed.
  7451. */
  7452. function parseAttributeStatement (graph) {
  7453. // attribute statements
  7454. if (token == 'node') {
  7455. getToken();
  7456. // node attributes
  7457. graph.node = parseAttributeList();
  7458. return 'node';
  7459. }
  7460. else if (token == 'edge') {
  7461. getToken();
  7462. // edge attributes
  7463. graph.edge = parseAttributeList();
  7464. return 'edge';
  7465. }
  7466. else if (token == 'graph') {
  7467. getToken();
  7468. // graph attributes
  7469. graph.graph = parseAttributeList();
  7470. return 'graph';
  7471. }
  7472. return null;
  7473. }
  7474. /**
  7475. * parse a node statement
  7476. * @param {Object} graph
  7477. * @param {String | Number} id
  7478. */
  7479. function parseNodeStatement(graph, id) {
  7480. // node statement
  7481. var node = {
  7482. id: id
  7483. };
  7484. var attr = parseAttributeList();
  7485. if (attr) {
  7486. node.attr = attr;
  7487. }
  7488. addNode(graph, node);
  7489. // edge statements
  7490. parseEdge(graph, id);
  7491. }
  7492. /**
  7493. * Parse an edge or a series of edges
  7494. * @param {Object} graph
  7495. * @param {String | Number} from Id of the from node
  7496. */
  7497. function parseEdge(graph, from) {
  7498. while (token == '->' || token == '--') {
  7499. var to;
  7500. var type = token;
  7501. getToken();
  7502. var subgraph = parseSubgraph(graph);
  7503. if (subgraph) {
  7504. to = subgraph;
  7505. }
  7506. else {
  7507. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7508. throw newSyntaxError('Identifier or subgraph expected');
  7509. }
  7510. to = token;
  7511. addNode(graph, {
  7512. id: to
  7513. });
  7514. getToken();
  7515. }
  7516. // parse edge attributes
  7517. var attr = parseAttributeList();
  7518. // create edge
  7519. var edge = createEdge(graph, from, to, type, attr);
  7520. addEdge(graph, edge);
  7521. from = to;
  7522. }
  7523. }
  7524. /**
  7525. * Parse a set with attributes,
  7526. * for example [label="1.000", shape=solid]
  7527. * @return {Object | null} attr
  7528. */
  7529. function parseAttributeList() {
  7530. var attr = null;
  7531. while (token == '[') {
  7532. getToken();
  7533. attr = {};
  7534. while (token !== '' && token != ']') {
  7535. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7536. throw newSyntaxError('Attribute name expected');
  7537. }
  7538. var name = token;
  7539. getToken();
  7540. if (token != '=') {
  7541. throw newSyntaxError('Equal sign = expected');
  7542. }
  7543. getToken();
  7544. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7545. throw newSyntaxError('Attribute value expected');
  7546. }
  7547. var value = token;
  7548. setValue(attr, name, value); // name can be a path
  7549. getToken();
  7550. if (token ==',') {
  7551. getToken();
  7552. }
  7553. }
  7554. if (token != ']') {
  7555. throw newSyntaxError('Bracket ] expected');
  7556. }
  7557. getToken();
  7558. }
  7559. return attr;
  7560. }
  7561. /**
  7562. * Create a syntax error with extra information on current token and index.
  7563. * @param {String} message
  7564. * @returns {SyntaxError} err
  7565. */
  7566. function newSyntaxError(message) {
  7567. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7568. }
  7569. /**
  7570. * Chop off text after a maximum length
  7571. * @param {String} text
  7572. * @param {Number} maxLength
  7573. * @returns {String}
  7574. */
  7575. function chop (text, maxLength) {
  7576. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7577. }
  7578. /**
  7579. * Execute a function fn for each pair of elements in two arrays
  7580. * @param {Array | *} array1
  7581. * @param {Array | *} array2
  7582. * @param {function} fn
  7583. */
  7584. function forEach2(array1, array2, fn) {
  7585. if (array1 instanceof Array) {
  7586. array1.forEach(function (elem1) {
  7587. if (array2 instanceof Array) {
  7588. array2.forEach(function (elem2) {
  7589. fn(elem1, elem2);
  7590. });
  7591. }
  7592. else {
  7593. fn(elem1, array2);
  7594. }
  7595. });
  7596. }
  7597. else {
  7598. if (array2 instanceof Array) {
  7599. array2.forEach(function (elem2) {
  7600. fn(array1, elem2);
  7601. });
  7602. }
  7603. else {
  7604. fn(array1, array2);
  7605. }
  7606. }
  7607. }
  7608. /**
  7609. * Convert a string containing a graph in DOT language into a map containing
  7610. * with nodes and edges in the format of graph.
  7611. * @param {String} data Text containing a graph in DOT-notation
  7612. * @return {Object} graphData
  7613. */
  7614. function DOTToGraph (data) {
  7615. // parse the DOT file
  7616. var dotData = parseDOT(data);
  7617. var graphData = {
  7618. nodes: [],
  7619. edges: [],
  7620. options: {}
  7621. };
  7622. // copy the nodes
  7623. if (dotData.nodes) {
  7624. dotData.nodes.forEach(function (dotNode) {
  7625. var graphNode = {
  7626. id: dotNode.id,
  7627. label: String(dotNode.label || dotNode.id)
  7628. };
  7629. merge(graphNode, dotNode.attr);
  7630. if (graphNode.image) {
  7631. graphNode.shape = 'image';
  7632. }
  7633. graphData.nodes.push(graphNode);
  7634. });
  7635. }
  7636. // copy the edges
  7637. if (dotData.edges) {
  7638. /**
  7639. * Convert an edge in DOT format to an edge with VisGraph format
  7640. * @param {Object} dotEdge
  7641. * @returns {Object} graphEdge
  7642. */
  7643. function convertEdge(dotEdge) {
  7644. var graphEdge = {
  7645. from: dotEdge.from,
  7646. to: dotEdge.to
  7647. };
  7648. merge(graphEdge, dotEdge.attr);
  7649. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7650. return graphEdge;
  7651. }
  7652. dotData.edges.forEach(function (dotEdge) {
  7653. var from, to;
  7654. if (dotEdge.from instanceof Object) {
  7655. from = dotEdge.from.nodes;
  7656. }
  7657. else {
  7658. from = {
  7659. id: dotEdge.from
  7660. }
  7661. }
  7662. if (dotEdge.to instanceof Object) {
  7663. to = dotEdge.to.nodes;
  7664. }
  7665. else {
  7666. to = {
  7667. id: dotEdge.to
  7668. }
  7669. }
  7670. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7671. dotEdge.from.edges.forEach(function (subEdge) {
  7672. var graphEdge = convertEdge(subEdge);
  7673. graphData.edges.push(graphEdge);
  7674. });
  7675. }
  7676. forEach2(from, to, function (from, to) {
  7677. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7678. var graphEdge = convertEdge(subEdge);
  7679. graphData.edges.push(graphEdge);
  7680. });
  7681. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7682. dotEdge.to.edges.forEach(function (subEdge) {
  7683. var graphEdge = convertEdge(subEdge);
  7684. graphData.edges.push(graphEdge);
  7685. });
  7686. }
  7687. });
  7688. }
  7689. // copy the options
  7690. if (dotData.attr) {
  7691. graphData.options = dotData.attr;
  7692. }
  7693. return graphData;
  7694. }
  7695. // exports
  7696. exports.parseDOT = parseDOT;
  7697. exports.DOTToGraph = DOTToGraph;
  7698. })(typeof util !== 'undefined' ? util : exports);
  7699. /**
  7700. * Canvas shapes used by the Graph
  7701. */
  7702. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7703. /**
  7704. * Draw a circle shape
  7705. */
  7706. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7707. this.beginPath();
  7708. this.arc(x, y, r, 0, 2*Math.PI, false);
  7709. };
  7710. /**
  7711. * Draw a square shape
  7712. * @param {Number} x horizontal center
  7713. * @param {Number} y vertical center
  7714. * @param {Number} r size, width and height of the square
  7715. */
  7716. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7717. this.beginPath();
  7718. this.rect(x - r, y - r, r * 2, r * 2);
  7719. };
  7720. /**
  7721. * Draw a triangle shape
  7722. * @param {Number} x horizontal center
  7723. * @param {Number} y vertical center
  7724. * @param {Number} r radius, half the length of the sides of the triangle
  7725. */
  7726. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7727. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7728. this.beginPath();
  7729. var s = r * 2;
  7730. var s2 = s / 2;
  7731. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7732. var h = Math.sqrt(s * s - s2 * s2); // height
  7733. this.moveTo(x, y - (h - ir));
  7734. this.lineTo(x + s2, y + ir);
  7735. this.lineTo(x - s2, y + ir);
  7736. this.lineTo(x, y - (h - ir));
  7737. this.closePath();
  7738. };
  7739. /**
  7740. * Draw a triangle shape in downward orientation
  7741. * @param {Number} x horizontal center
  7742. * @param {Number} y vertical center
  7743. * @param {Number} r radius
  7744. */
  7745. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7746. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7747. this.beginPath();
  7748. var s = r * 2;
  7749. var s2 = s / 2;
  7750. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7751. var h = Math.sqrt(s * s - s2 * s2); // height
  7752. this.moveTo(x, y + (h - ir));
  7753. this.lineTo(x + s2, y - ir);
  7754. this.lineTo(x - s2, y - ir);
  7755. this.lineTo(x, y + (h - ir));
  7756. this.closePath();
  7757. };
  7758. /**
  7759. * Draw a star shape, a star with 5 points
  7760. * @param {Number} x horizontal center
  7761. * @param {Number} y vertical center
  7762. * @param {Number} r radius, half the length of the sides of the triangle
  7763. */
  7764. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7765. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7766. this.beginPath();
  7767. for (var n = 0; n < 10; n++) {
  7768. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7769. this.lineTo(
  7770. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7771. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7772. );
  7773. }
  7774. this.closePath();
  7775. };
  7776. /**
  7777. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7778. */
  7779. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7780. var r2d = Math.PI/180;
  7781. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7782. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7783. this.beginPath();
  7784. this.moveTo(x+r,y);
  7785. this.lineTo(x+w-r,y);
  7786. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7787. this.lineTo(x+w,y+h-r);
  7788. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7789. this.lineTo(x+r,y+h);
  7790. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7791. this.lineTo(x,y+r);
  7792. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7793. };
  7794. /**
  7795. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7796. */
  7797. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7798. var kappa = .5522848,
  7799. ox = (w / 2) * kappa, // control point offset horizontal
  7800. oy = (h / 2) * kappa, // control point offset vertical
  7801. xe = x + w, // x-end
  7802. ye = y + h, // y-end
  7803. xm = x + w / 2, // x-middle
  7804. ym = y + h / 2; // y-middle
  7805. this.beginPath();
  7806. this.moveTo(x, ym);
  7807. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7808. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7809. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7810. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7811. };
  7812. /**
  7813. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7814. */
  7815. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7816. var f = 1/3;
  7817. var wEllipse = w;
  7818. var hEllipse = h * f;
  7819. var kappa = .5522848,
  7820. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7821. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7822. xe = x + wEllipse, // x-end
  7823. ye = y + hEllipse, // y-end
  7824. xm = x + wEllipse / 2, // x-middle
  7825. ym = y + hEllipse / 2, // y-middle
  7826. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7827. yeb = y + h; // y-end, bottom ellipse
  7828. this.beginPath();
  7829. this.moveTo(xe, ym);
  7830. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7831. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7832. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7833. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7834. this.lineTo(xe, ymb);
  7835. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7836. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7837. this.lineTo(x, ym);
  7838. };
  7839. /**
  7840. * Draw an arrow point (no line)
  7841. */
  7842. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7843. // tail
  7844. var xt = x - length * Math.cos(angle);
  7845. var yt = y - length * Math.sin(angle);
  7846. // inner tail
  7847. // TODO: allow to customize different shapes
  7848. var xi = x - length * 0.9 * Math.cos(angle);
  7849. var yi = y - length * 0.9 * Math.sin(angle);
  7850. // left
  7851. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7852. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7853. // right
  7854. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7855. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7856. this.beginPath();
  7857. this.moveTo(x, y);
  7858. this.lineTo(xl, yl);
  7859. this.lineTo(xi, yi);
  7860. this.lineTo(xr, yr);
  7861. this.closePath();
  7862. };
  7863. /**
  7864. * Sets up the dashedLine functionality for drawing
  7865. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7866. * @author David Jordan
  7867. * @date 2012-08-08
  7868. */
  7869. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7870. if (!dashArray) dashArray=[10,5];
  7871. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7872. var dashCount = dashArray.length;
  7873. this.moveTo(x, y);
  7874. var dx = (x2-x), dy = (y2-y);
  7875. var slope = dy/dx;
  7876. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7877. var dashIndex=0, draw=true;
  7878. while (distRemaining>=0.1){
  7879. var dashLength = dashArray[dashIndex++%dashCount];
  7880. if (dashLength > distRemaining) dashLength = distRemaining;
  7881. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7882. if (dx<0) xStep = -xStep;
  7883. x += xStep;
  7884. y += slope*xStep;
  7885. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7886. distRemaining -= dashLength;
  7887. draw = !draw;
  7888. }
  7889. };
  7890. // TODO: add diamond shape
  7891. }
  7892. /**
  7893. * @class Node
  7894. * A node. A node can be connected to other nodes via one or multiple edges.
  7895. * @param {object} properties An object containing properties for the node. All
  7896. * properties are optional, except for the id.
  7897. * {number} id Id of the node. Required
  7898. * {string} label Text label for the node
  7899. * {number} x Horizontal position of the node
  7900. * {number} y Vertical position of the node
  7901. * {string} shape Node shape, available:
  7902. * "database", "circle", "ellipse",
  7903. * "box", "image", "text", "dot",
  7904. * "star", "triangle", "triangleDown",
  7905. * "square"
  7906. * {string} image An image url
  7907. * {string} title An title text, can be HTML
  7908. * {anytype} group A group name or number
  7909. * @param {Graph.Images} imagelist A list with images. Only needed
  7910. * when the node has an image
  7911. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7912. * retrieving group properties
  7913. * @param {Object} constants An object with default values for
  7914. * example for the color
  7915. */
  7916. function Node(properties, imagelist, grouplist, constants) {
  7917. this.selected = false;
  7918. this.edges = []; // all edges connected to this node
  7919. this.dynamicEdges = [];
  7920. this.reroutedEdges = {};
  7921. this.group = constants.nodes.group;
  7922. this.fontSize = constants.nodes.fontSize;
  7923. this.fontFace = constants.nodes.fontFace;
  7924. this.fontColor = constants.nodes.fontColor;
  7925. this.color = constants.nodes.color;
  7926. // set defaults for the properties
  7927. this.id = undefined;
  7928. this.shape = constants.nodes.shape;
  7929. this.image = constants.nodes.image;
  7930. this.x = 0;
  7931. this.y = 0;
  7932. this.xFixed = false;
  7933. this.yFixed = false;
  7934. this.horizontalAlignLeft = true; // these are for the navigation controls
  7935. this.verticalAlignTop = true; // these are for the navigation controls
  7936. this.radius = constants.nodes.radius;
  7937. this.baseRadiusValue = constants.nodes.radius;
  7938. this.radiusFixed = false;
  7939. this.radiusMin = constants.nodes.radiusMin;
  7940. this.radiusMax = constants.nodes.radiusMax;
  7941. this.imagelist = imagelist;
  7942. this.grouplist = grouplist;
  7943. this.setProperties(properties, constants);
  7944. // creating the variables for clustering
  7945. this.resetCluster();
  7946. this.dynamicEdgesLength = 0;
  7947. this.clusterSession = 0;
  7948. this.clusterSizeWidthFactor = constants.clustering.nodeScaling.width;
  7949. this.clusterSizeHeightFactor = constants.clustering.nodeScaling.height;
  7950. this.clusterSizeRadiusFactor = constants.clustering.nodeScaling.radius;
  7951. // mass, force, velocity
  7952. this.mass = 1; // kg (mass is adjusted for the number of connected edges)
  7953. this.fx = 0.0; // external force x
  7954. this.fy = 0.0; // external force y
  7955. this.vx = 0.0; // velocity x
  7956. this.vy = 0.0; // velocity y
  7957. this.minForce = constants.minForce;
  7958. this.damping = 0.9;
  7959. this.dampingFactor = 75;
  7960. this.graphScaleInv = 1;
  7961. this.canvasTopLeft = {"x": -300, "y": -300};
  7962. this.canvasBottomRight = {"x": 300, "y": 300};
  7963. }
  7964. /**
  7965. * (re)setting the clustering variables and objects
  7966. */
  7967. Node.prototype.resetCluster = function() {
  7968. // clustering variables
  7969. this.formationScale = undefined; // this is used to determine when to open the cluster
  7970. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  7971. this.containedNodes = {};
  7972. this.containedEdges = {};
  7973. this.clusterSessions = [];
  7974. };
  7975. /**
  7976. * Attach a edge to the node
  7977. * @param {Edge} edge
  7978. */
  7979. Node.prototype.attachEdge = function(edge) {
  7980. if (this.edges.indexOf(edge) == -1) {
  7981. this.edges.push(edge);
  7982. }
  7983. if (this.dynamicEdges.indexOf(edge) == -1) {
  7984. this.dynamicEdges.push(edge);
  7985. }
  7986. this.dynamicEdgesLength = this.dynamicEdges.length;
  7987. this._updateMass();
  7988. };
  7989. /**
  7990. * Detach a edge from the node
  7991. * @param {Edge} edge
  7992. */
  7993. Node.prototype.detachEdge = function(edge) {
  7994. var index = this.edges.indexOf(edge);
  7995. if (index != -1) {
  7996. this.edges.splice(index, 1);
  7997. this.dynamicEdges.splice(index, 1);
  7998. }
  7999. this.dynamicEdgesLength = this.dynamicEdges.length;
  8000. this._updateMass();
  8001. };
  8002. /**
  8003. * Update the nodes mass, which is determined by the number of edges connecting
  8004. * to it (more edges -> heavier node).
  8005. * @private
  8006. */
  8007. Node.prototype._updateMass = function() {
  8008. this.mass = 1 + 0.6 * this.edges.length; // kg
  8009. };
  8010. /**
  8011. * Set or overwrite properties for the node
  8012. * @param {Object} properties an object with properties
  8013. * @param {Object} constants and object with default, global properties
  8014. */
  8015. Node.prototype.setProperties = function(properties, constants) {
  8016. if (!properties) {
  8017. return;
  8018. }
  8019. this.originalLabel = undefined;
  8020. // basic properties
  8021. if (properties.id !== undefined) {this.id = properties.id;}
  8022. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  8023. if (properties.title !== undefined) {this.title = properties.title;}
  8024. if (properties.group !== undefined) {this.group = properties.group;}
  8025. if (properties.x !== undefined) {this.x = properties.x;}
  8026. if (properties.y !== undefined) {this.y = properties.y;}
  8027. if (properties.value !== undefined) {this.value = properties.value;}
  8028. // navigation controls properties
  8029. if (properties.horizontalAlignLeft !== undefined) {this.horizontalAlignLeft = properties.horizontalAlignLeft;}
  8030. if (properties.verticalAlignTop !== undefined) {this.verticalAlignTop = properties.verticalAlignTop;}
  8031. if (properties.triggerFunction !== undefined) {this.triggerFunction = properties.triggerFunction;}
  8032. if (this.id === undefined) {
  8033. throw "Node must have an id";
  8034. }
  8035. // copy group properties
  8036. if (this.group) {
  8037. var groupObj = this.grouplist.get(this.group);
  8038. for (var prop in groupObj) {
  8039. if (groupObj.hasOwnProperty(prop)) {
  8040. this[prop] = groupObj[prop];
  8041. }
  8042. }
  8043. }
  8044. // individual shape properties
  8045. if (properties.shape !== undefined) {this.shape = properties.shape;}
  8046. if (properties.image !== undefined) {this.image = properties.image;}
  8047. if (properties.radius !== undefined) {this.radius = properties.radius;}
  8048. if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);}
  8049. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8050. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8051. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8052. if (this.image !== undefined) {
  8053. if (this.imagelist) {
  8054. this.imageObj = this.imagelist.load(this.image);
  8055. }
  8056. else {
  8057. throw "No imagelist provided";
  8058. }
  8059. }
  8060. this.xFixed = this.xFixed || (properties.x !== undefined);
  8061. this.yFixed = this.yFixed || (properties.y !== undefined);
  8062. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  8063. if (this.shape == 'image') {
  8064. this.radiusMin = constants.nodes.widthMin;
  8065. this.radiusMax = constants.nodes.widthMax;
  8066. }
  8067. // choose draw method depending on the shape
  8068. switch (this.shape) {
  8069. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  8070. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  8071. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  8072. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8073. // TODO: add diamond shape
  8074. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  8075. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  8076. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  8077. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  8078. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  8079. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  8080. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  8081. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  8082. }
  8083. // reset the size of the node, this can be changed
  8084. this._reset();
  8085. };
  8086. /**
  8087. * Parse a color property into an object with border, background, and
  8088. * hightlight colors
  8089. * @param {Object | String} color
  8090. * @return {Object} colorObject
  8091. */
  8092. Node.parseColor = function(color) {
  8093. var c;
  8094. if (util.isString(color)) {
  8095. c = {
  8096. border: color,
  8097. background: color,
  8098. highlight: {
  8099. border: color,
  8100. background: color
  8101. }
  8102. };
  8103. // TODO: automatically generate a nice highlight color
  8104. }
  8105. else {
  8106. c = {};
  8107. c.background = color.background || 'white';
  8108. c.border = color.border || c.background;
  8109. if (util.isString(color.highlight)) {
  8110. c.highlight = {
  8111. border: color.highlight,
  8112. background: color.highlight
  8113. }
  8114. }
  8115. else {
  8116. c.highlight = {};
  8117. c.highlight.background = color.highlight && color.highlight.background || c.background;
  8118. c.highlight.border = color.highlight && color.highlight.border || c.border;
  8119. }
  8120. }
  8121. return c;
  8122. };
  8123. /**
  8124. * select this node
  8125. */
  8126. Node.prototype.select = function() {
  8127. this.selected = true;
  8128. this._reset();
  8129. };
  8130. /**
  8131. * unselect this node
  8132. */
  8133. Node.prototype.unselect = function() {
  8134. this.selected = false;
  8135. this._reset();
  8136. };
  8137. /**
  8138. * Reset the calculated size of the node, forces it to recalculate its size
  8139. */
  8140. Node.prototype.clearSizeCache = function() {
  8141. this._reset();
  8142. };
  8143. /**
  8144. * Reset the calculated size of the node, forces it to recalculate its size
  8145. * @private
  8146. */
  8147. Node.prototype._reset = function() {
  8148. this.width = undefined;
  8149. this.height = undefined;
  8150. };
  8151. /**
  8152. * get the title of this node.
  8153. * @return {string} title The title of the node, or undefined when no title
  8154. * has been set.
  8155. */
  8156. Node.prototype.getTitle = function() {
  8157. return this.title;
  8158. };
  8159. /**
  8160. * Calculate the distance to the border of the Node
  8161. * @param {CanvasRenderingContext2D} ctx
  8162. * @param {Number} angle Angle in radians
  8163. * @returns {number} distance Distance to the border in pixels
  8164. */
  8165. Node.prototype.distanceToBorder = function (ctx, angle) {
  8166. var borderWidth = 1;
  8167. if (!this.width) {
  8168. this.resize(ctx);
  8169. }
  8170. //noinspection FallthroughInSwitchStatementJS
  8171. switch (this.shape) {
  8172. case 'circle':
  8173. case 'dot':
  8174. return this.radius + borderWidth;
  8175. case 'ellipse':
  8176. var a = this.width / 2;
  8177. var b = this.height / 2;
  8178. var w = (Math.sin(angle) * a);
  8179. var h = (Math.cos(angle) * b);
  8180. return a * b / Math.sqrt(w * w + h * h);
  8181. // TODO: implement distanceToBorder for database
  8182. // TODO: implement distanceToBorder for triangle
  8183. // TODO: implement distanceToBorder for triangleDown
  8184. case 'box':
  8185. case 'image':
  8186. case 'text':
  8187. default:
  8188. if (this.width) {
  8189. return Math.min(
  8190. Math.abs(this.width / 2 / Math.cos(angle)),
  8191. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  8192. // TODO: reckon with border radius too in case of box
  8193. }
  8194. else {
  8195. return 0;
  8196. }
  8197. }
  8198. // TODO: implement calculation of distance to border for all shapes
  8199. };
  8200. /**
  8201. * Set forces acting on the node
  8202. * @param {number} fx Force in horizontal direction
  8203. * @param {number} fy Force in vertical direction
  8204. */
  8205. Node.prototype._setForce = function(fx, fy) {
  8206. this.fx = fx;
  8207. this.fy = fy;
  8208. };
  8209. /**
  8210. * Add forces acting on the node
  8211. * @param {number} fx Force in horizontal direction
  8212. * @param {number} fy Force in vertical direction
  8213. * @private
  8214. */
  8215. Node.prototype._addForce = function(fx, fy) {
  8216. this.fx += fx;
  8217. this.fy += fy;
  8218. };
  8219. /**
  8220. * Perform one discrete step for the node
  8221. * @param {number} interval Time interval in seconds
  8222. */
  8223. Node.prototype.discreteStep = function(interval) {
  8224. if (!this.xFixed) {
  8225. var dx = -this.damping * this.vx; // damping force
  8226. var ax = (this.fx + dx) / this.mass; // acceleration
  8227. this.vx += ax * interval; // velocity
  8228. this.x += this.vx * interval; // position
  8229. }
  8230. if (!this.yFixed) {
  8231. var dy = -this.damping * this.vy; // damping force
  8232. var ay = (this.fy + dy) / this.mass; // acceleration
  8233. this.vy += ay * interval; // velocity
  8234. this.y += this.vy * interval; // position
  8235. }
  8236. };
  8237. /**
  8238. * Check if this node has a fixed x and y position
  8239. * @return {boolean} true if fixed, false if not
  8240. */
  8241. Node.prototype.isFixed = function() {
  8242. return (this.xFixed && this.yFixed);
  8243. };
  8244. /**
  8245. * Check if this node is moving
  8246. * @param {number} vmin the minimum velocity considered as "moving"
  8247. * @return {boolean} true if moving, false if it has no velocity
  8248. */
  8249. // TODO: replace this method with calculating the kinetic energy
  8250. Node.prototype.isMoving = function(vmin) {
  8251. if (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin) {
  8252. // console.log(vmin,this.vx,this.vy);
  8253. return true;
  8254. }
  8255. else {
  8256. this.vx = 0; this.vy = 0;
  8257. return false;
  8258. }
  8259. //return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin);
  8260. };
  8261. /**
  8262. * check if this node is selecte
  8263. * @return {boolean} selected True if node is selected, else false
  8264. */
  8265. Node.prototype.isSelected = function() {
  8266. return this.selected;
  8267. };
  8268. /**
  8269. * Retrieve the value of the node. Can be undefined
  8270. * @return {Number} value
  8271. */
  8272. Node.prototype.getValue = function() {
  8273. return this.value;
  8274. };
  8275. /**
  8276. * Calculate the distance from the nodes location to the given location (x,y)
  8277. * @param {Number} x
  8278. * @param {Number} y
  8279. * @return {Number} value
  8280. */
  8281. Node.prototype.getDistance = function(x, y) {
  8282. var dx = this.x - x,
  8283. dy = this.y - y;
  8284. return Math.sqrt(dx * dx + dy * dy);
  8285. };
  8286. /**
  8287. * Adjust the value range of the node. The node will adjust it's radius
  8288. * based on its value.
  8289. * @param {Number} min
  8290. * @param {Number} max
  8291. */
  8292. Node.prototype.setValueRange = function(min, max) {
  8293. if (!this.radiusFixed && this.value !== undefined) {
  8294. if (max == min) {
  8295. this.radius = (this.radiusMin + this.radiusMax) / 2;
  8296. }
  8297. else {
  8298. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8299. this.radius = (this.value - min) * scale + this.radiusMin;
  8300. }
  8301. }
  8302. this.baseRadiusValue = this.radius;
  8303. };
  8304. /**
  8305. * Draw this node in the given canvas
  8306. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8307. * @param {CanvasRenderingContext2D} ctx
  8308. */
  8309. Node.prototype.draw = function(ctx) {
  8310. throw "Draw method not initialized for node";
  8311. };
  8312. /**
  8313. * Recalculate the size of this node in the given canvas
  8314. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8315. * @param {CanvasRenderingContext2D} ctx
  8316. */
  8317. Node.prototype.resize = function(ctx) {
  8318. throw "Resize method not initialized for node";
  8319. };
  8320. /**
  8321. * Check if this object is overlapping with the provided object
  8322. * @param {Object} obj an object with parameters left, top, right, bottom
  8323. * @return {boolean} True if location is located on node
  8324. */
  8325. Node.prototype.isOverlappingWith = function(obj) {
  8326. return (this.left < obj.right &&
  8327. this.left + this.width > obj.left &&
  8328. this.top < obj.bottom &&
  8329. this.top + this.height > obj.top);
  8330. };
  8331. Node.prototype._resizeImage = function (ctx) {
  8332. // TODO: pre calculate the image size
  8333. if (!this.width || !this.height) { // undefined or 0
  8334. var width, height;
  8335. if (this.value) {
  8336. this.radius = this.baseRadiusValue;
  8337. var scale = this.imageObj.height / this.imageObj.width;
  8338. if (scale !== undefined) {
  8339. width = this.radius || this.imageObj.width;
  8340. height = this.radius * scale || this.imageObj.height;
  8341. }
  8342. else {
  8343. width = 0;
  8344. height = 0;
  8345. }
  8346. }
  8347. else {
  8348. width = this.imageObj.width;
  8349. height = this.imageObj.height;
  8350. }
  8351. this.width = width;
  8352. this.height = height;
  8353. if (this.width > 0 && this.height > 0) {
  8354. this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
  8355. this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
  8356. this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
  8357. }
  8358. }
  8359. };
  8360. Node.prototype._drawImage = function (ctx) {
  8361. this._resizeImage(ctx);
  8362. this.left = this.x - this.width / 2;
  8363. this.top = this.y - this.height / 2;
  8364. var yLabel;
  8365. if (this.imageObj.width != 0 ) {
  8366. // draw the shade
  8367. if (this.clusterSize > 1) {
  8368. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  8369. lineWidth *= this.graphScaleInv;
  8370. lineWidth = Math.min(0.2 * this.width,lineWidth);
  8371. ctx.globalAlpha = 0.5;
  8372. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  8373. }
  8374. // draw the image
  8375. ctx.globalAlpha = 1.0;
  8376. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8377. yLabel = this.y + this.height / 2;
  8378. }
  8379. else {
  8380. // image still loading... just draw the label for now
  8381. yLabel = this.y;
  8382. }
  8383. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8384. };
  8385. Node.prototype._resizeBox = function (ctx) {
  8386. if (!this.width) {
  8387. var margin = 5;
  8388. var textSize = this.getTextSize(ctx);
  8389. this.width = textSize.width + 2 * margin;
  8390. this.height = textSize.height + 2 * margin;
  8391. this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
  8392. this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
  8393. // this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
  8394. }
  8395. };
  8396. Node.prototype._drawBox = function (ctx) {
  8397. this._resizeBox(ctx);
  8398. this.left = this.x - this.width / 2;
  8399. this.top = this.y - this.height / 2;
  8400. var clusterLineWidth = 2.5;
  8401. var selectionLineWidth = 2;
  8402. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8403. // draw the outer border
  8404. if (this.clusterSize > 1) {
  8405. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8406. ctx.lineWidth *= this.graphScaleInv;
  8407. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8408. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  8409. ctx.stroke();
  8410. }
  8411. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8412. ctx.lineWidth *= this.graphScaleInv;
  8413. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8414. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8415. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8416. ctx.fill();
  8417. ctx.stroke();
  8418. this._label(ctx, this.label, this.x, this.y);
  8419. };
  8420. Node.prototype._resizeDatabase = function (ctx) {
  8421. if (!this.width) {
  8422. var margin = 5;
  8423. var textSize = this.getTextSize(ctx);
  8424. var size = textSize.width + 2 * margin;
  8425. this.width = size;
  8426. this.height = size;
  8427. // scaling used for clustering
  8428. this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
  8429. this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
  8430. this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
  8431. }
  8432. };
  8433. Node.prototype._drawDatabase = function (ctx) {
  8434. this._resizeDatabase(ctx);
  8435. this.left = this.x - this.width / 2;
  8436. this.top = this.y - this.height / 2;
  8437. var clusterLineWidth = 2.5;
  8438. var selectionLineWidth = 2;
  8439. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8440. // draw the outer border
  8441. if (this.clusterSize > 1) {
  8442. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8443. ctx.lineWidth *= this.graphScaleInv;
  8444. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8445. 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);
  8446. ctx.stroke();
  8447. }
  8448. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8449. ctx.lineWidth *= this.graphScaleInv;
  8450. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8451. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8452. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8453. ctx.fill();
  8454. ctx.stroke();
  8455. this._label(ctx, this.label, this.x, this.y);
  8456. };
  8457. Node.prototype._resizeCircle = function (ctx) {
  8458. if (!this.width) {
  8459. var margin = 5;
  8460. var textSize = this.getTextSize(ctx);
  8461. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8462. this.radius = diameter / 2;
  8463. this.width = diameter;
  8464. this.height = diameter;
  8465. // scaling used for clustering
  8466. // this.width += (this.clusterSize - 1) * 0.5 * this.clusterSizeWidthFactor;
  8467. // this.height += (this.clusterSize - 1) * 0.5 * this.clusterSizeHeightFactor;
  8468. this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
  8469. }
  8470. };
  8471. Node.prototype._drawCircle = function (ctx) {
  8472. this._resizeCircle(ctx);
  8473. this.left = this.x - this.width / 2;
  8474. this.top = this.y - this.height / 2;
  8475. var clusterLineWidth = 2.5;
  8476. var selectionLineWidth = 2;
  8477. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8478. // draw the outer border
  8479. if (this.clusterSize > 1) {
  8480. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8481. ctx.lineWidth *= this.graphScaleInv;
  8482. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8483. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  8484. ctx.stroke();
  8485. }
  8486. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8487. ctx.lineWidth *= this.graphScaleInv;
  8488. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8489. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8490. ctx.circle(this.x, this.y, this.radius);
  8491. ctx.fill();
  8492. ctx.stroke();
  8493. this._label(ctx, this.label, this.x, this.y);
  8494. };
  8495. Node.prototype._resizeEllipse = function (ctx) {
  8496. if (!this.width) {
  8497. var textSize = this.getTextSize(ctx);
  8498. this.width = textSize.width * 1.5;
  8499. this.height = textSize.height * 2;
  8500. if (this.width < this.height) {
  8501. this.width = this.height;
  8502. }
  8503. // scaling used for clustering
  8504. this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
  8505. this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
  8506. this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
  8507. }
  8508. };
  8509. Node.prototype._drawEllipse = function (ctx) {
  8510. this._resizeEllipse(ctx);
  8511. this.left = this.x - this.width / 2;
  8512. this.top = this.y - this.height / 2;
  8513. var clusterLineWidth = 2.5;
  8514. var selectionLineWidth = 2;
  8515. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8516. // draw the outer border
  8517. if (this.clusterSize > 1) {
  8518. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8519. ctx.lineWidth *= this.graphScaleInv;
  8520. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8521. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  8522. ctx.stroke();
  8523. }
  8524. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8525. ctx.lineWidth *= this.graphScaleInv;
  8526. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8527. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8528. ctx.ellipse(this.left, this.top, this.width, this.height);
  8529. ctx.fill();
  8530. ctx.stroke();
  8531. this._label(ctx, this.label, this.x, this.y);
  8532. };
  8533. Node.prototype._drawDot = function (ctx) {
  8534. this._drawShape(ctx, 'circle');
  8535. };
  8536. Node.prototype._drawTriangle = function (ctx) {
  8537. this._drawShape(ctx, 'triangle');
  8538. };
  8539. Node.prototype._drawTriangleDown = function (ctx) {
  8540. this._drawShape(ctx, 'triangleDown');
  8541. };
  8542. Node.prototype._drawSquare = function (ctx) {
  8543. this._drawShape(ctx, 'square');
  8544. };
  8545. Node.prototype._drawStar = function (ctx) {
  8546. this._drawShape(ctx, 'star');
  8547. };
  8548. Node.prototype._resizeShape = function (ctx) {
  8549. if (!this.width) {
  8550. this.radius = this.baseRadiusValue;
  8551. var size = 2 * this.radius;
  8552. this.width = size;
  8553. this.height = size;
  8554. // scaling used for clustering
  8555. this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
  8556. this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
  8557. this.radius += (this.clusterSize - 1) * 0.5 * this.clusterSizeRadiusFactor;
  8558. }
  8559. };
  8560. Node.prototype._drawShape = function (ctx, shape) {
  8561. this._resizeShape(ctx);
  8562. this.left = this.x - this.width / 2;
  8563. this.top = this.y - this.height / 2;
  8564. var clusterLineWidth = 2.5;
  8565. var selectionLineWidth = 2;
  8566. var radiusMultiplier = 2;
  8567. // choose draw method depending on the shape
  8568. switch (shape) {
  8569. case 'dot': radiusMultiplier = 2; break;
  8570. case 'square': radiusMultiplier = 2; break;
  8571. case 'triangle': radiusMultiplier = 3; break;
  8572. case 'triangleDown': radiusMultiplier = 3; break;
  8573. case 'star': radiusMultiplier = 4; break;
  8574. }
  8575. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8576. // draw the outer border
  8577. if (this.clusterSize > 1) {
  8578. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8579. ctx.lineWidth *= this.graphScaleInv;
  8580. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8581. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  8582. ctx.stroke();
  8583. }
  8584. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8585. ctx.lineWidth *= this.graphScaleInv;
  8586. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8587. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8588. ctx[shape](this.x, this.y, this.radius);
  8589. ctx.fill();
  8590. ctx.stroke();
  8591. if (this.label) {
  8592. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8593. }
  8594. };
  8595. Node.prototype._resizeText = function (ctx) {
  8596. if (!this.width) {
  8597. var margin = 5;
  8598. var textSize = this.getTextSize(ctx);
  8599. this.width = textSize.width + 2 * margin;
  8600. this.height = textSize.height + 2 * margin;
  8601. // scaling used for clustering
  8602. this.width += (this.clusterSize - 1) * this.clusterSizeWidthFactor;
  8603. this.height += (this.clusterSize - 1) * this.clusterSizeHeightFactor;
  8604. this.radius += (this.clusterSize - 1) * this.clusterSizeRadiusFactor;
  8605. }
  8606. };
  8607. Node.prototype._drawText = function (ctx) {
  8608. this._resizeText(ctx);
  8609. this.left = this.x - this.width / 2;
  8610. this.top = this.y - this.height / 2;
  8611. this._label(ctx, this.label, this.x, this.y);
  8612. };
  8613. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8614. if (text) {
  8615. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8616. ctx.fillStyle = this.fontColor || "black";
  8617. ctx.textAlign = align || "center";
  8618. ctx.textBaseline = baseline || "middle";
  8619. var lines = text.split('\n'),
  8620. lineCount = lines.length,
  8621. fontSize = (this.fontSize + 4),
  8622. yLine = y + (1 - lineCount) / 2 * fontSize;
  8623. for (var i = 0; i < lineCount; i++) {
  8624. ctx.fillText(lines[i], x, yLine);
  8625. yLine += fontSize;
  8626. }
  8627. }
  8628. };
  8629. Node.prototype.getTextSize = function(ctx) {
  8630. if (this.label !== undefined) {
  8631. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8632. var lines = this.label.split('\n'),
  8633. height = (this.fontSize + 4) * lines.length,
  8634. width = 0;
  8635. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8636. width = Math.max(width, ctx.measureText(lines[i]).width);
  8637. }
  8638. return {"width": width, "height": height};
  8639. }
  8640. else {
  8641. return {"width": 0, "height": 0};
  8642. }
  8643. };
  8644. /**
  8645. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  8646. * there is a safety margin of 0.3 * width;
  8647. *
  8648. * @returns {boolean}
  8649. */
  8650. Node.prototype.inArea = function() {
  8651. if (this.width !== undefined) {
  8652. return (this.x + this.width*this.graphScaleInv >= this.canvasTopLeft.x &&
  8653. this.x - this.width*this.graphScaleInv < this.canvasBottomRight.x &&
  8654. this.y + this.height*this.graphScaleInv >= this.canvasTopLeft.y &&
  8655. this.y - this.height*this.graphScaleInv < this.canvasBottomRight.y);
  8656. }
  8657. else {
  8658. return true;
  8659. }
  8660. }
  8661. /**
  8662. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  8663. * @returns {boolean}
  8664. */
  8665. Node.prototype.inView = function() {
  8666. return (this.x >= this.canvasTopLeft.x &&
  8667. this.x < this.canvasBottomRight.x &&
  8668. this.y >= this.canvasTopLeft.y &&
  8669. this.y < this.canvasBottomRight.y);
  8670. }
  8671. /**
  8672. * This allows the zoom level of the graph to influence the rendering
  8673. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  8674. *
  8675. * @param scale
  8676. * @param canvasTopLeft
  8677. * @param canvasBottomRight
  8678. */
  8679. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  8680. this.graphScaleInv = 1.0/scale;
  8681. this.canvasTopLeft = canvasTopLeft;
  8682. this.canvasBottomRight = canvasBottomRight;
  8683. };
  8684. /**
  8685. * This allows the zoom level of the graph to influence the rendering
  8686. *
  8687. * @param scale
  8688. */
  8689. Node.prototype.setScale = function(scale) {
  8690. this.graphScaleInv = 1.0/scale;
  8691. };
  8692. /**
  8693. * This function updates the damping parameter for clusters, based ont he
  8694. *
  8695. * @param {Number} numberOfNodes
  8696. */
  8697. Node.prototype.updateDamping = function(numberOfNodes) {
  8698. this.damping = (0.8 + 0.1*this.clusterSize * (1 + Math.pow(numberOfNodes,-2)));
  8699. this.damping *= this.dampingFactor;
  8700. };
  8701. /**
  8702. * set the velocity at 0. Is called when this node is contained in another during clustering
  8703. */
  8704. Node.prototype.clearVelocity = function() {
  8705. this.vx = 0;
  8706. this.vy = 0;
  8707. };
  8708. /**
  8709. * Basic preservation of (kinectic) energy
  8710. *
  8711. * @param massBeforeClustering
  8712. */
  8713. Node.prototype.updateVelocity = function(massBeforeClustering) {
  8714. var energyBefore = this.vx * this.vx * massBeforeClustering;
  8715. this.vx = Math.sqrt(energyBefore/this.mass);
  8716. energyBefore = this.vy * this.vy * massBeforeClustering;
  8717. this.vy = Math.sqrt(energyBefore/this.mass);
  8718. };
  8719. /**
  8720. * @class Edge
  8721. *
  8722. * A edge connects two nodes
  8723. * @param {Object} properties Object with properties. Must contain
  8724. * At least properties from and to.
  8725. * Available properties: from (number),
  8726. * to (number), label (string, color (string),
  8727. * width (number), style (string),
  8728. * length (number), title (string)
  8729. * @param {Graph} graph A graph object, used to find and edge to
  8730. * nodes.
  8731. * @param {Object} constants An object with default values for
  8732. * example for the color
  8733. */
  8734. function Edge (properties, graph, constants) {
  8735. if (!graph) {
  8736. throw "No graph provided";
  8737. }
  8738. this.graph = graph;
  8739. // initialize constants
  8740. this.widthMin = constants.edges.widthMin;
  8741. this.widthMax = constants.edges.widthMax;
  8742. // initialize variables
  8743. this.id = undefined;
  8744. this.fromId = undefined;
  8745. this.toId = undefined;
  8746. this.style = constants.edges.style;
  8747. this.title = undefined;
  8748. this.width = constants.edges.width;
  8749. this.value = undefined;
  8750. this.length = constants.edges.length;
  8751. this.from = null; // a node
  8752. this.to = null; // a node
  8753. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  8754. // by storing the original information we can revert to the original connection when the cluser is opened.
  8755. this.originalFromId = [];
  8756. this.originalToId = [];
  8757. this.connected = false;
  8758. // Added to support dashed lines
  8759. // David Jordan
  8760. // 2012-08-08
  8761. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8762. this.stiffness = undefined; // depends on the length of the edge
  8763. this.color = constants.edges.color;
  8764. this.widthFixed = false;
  8765. this.lengthFixed = false;
  8766. this.setProperties(properties, constants);
  8767. }
  8768. /**
  8769. * Set or overwrite properties for the edge
  8770. * @param {Object} properties an object with properties
  8771. * @param {Object} constants and object with default, global properties
  8772. */
  8773. Edge.prototype.setProperties = function(properties, constants) {
  8774. if (!properties) {
  8775. return;
  8776. }
  8777. if (properties.from !== undefined) {this.fromId = properties.from;}
  8778. if (properties.to !== undefined) {this.toId = properties.to;}
  8779. if (properties.id !== undefined) {this.id = properties.id;}
  8780. if (properties.style !== undefined) {this.style = properties.style;}
  8781. if (properties.label !== undefined) {this.label = properties.label;}
  8782. if (this.label) {
  8783. this.fontSize = constants.edges.fontSize;
  8784. this.fontFace = constants.edges.fontFace;
  8785. this.fontColor = constants.edges.fontColor;
  8786. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  8787. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  8788. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  8789. }
  8790. if (properties.title !== undefined) {this.title = properties.title;}
  8791. if (properties.width !== undefined) {this.width = properties.width;}
  8792. if (properties.value !== undefined) {this.value = properties.value;}
  8793. if (properties.length !== undefined) {this.length = properties.length;}
  8794. // Added to support dashed lines
  8795. // David Jordan
  8796. // 2012-08-08
  8797. if (properties.dash) {
  8798. if (properties.dash.length !== undefined) {this.dash.length = properties.dash.length;}
  8799. if (properties.dash.gap !== undefined) {this.dash.gap = properties.dash.gap;}
  8800. if (properties.dash.altLength !== undefined) {this.dash.altLength = properties.dash.altLength;}
  8801. }
  8802. if (properties.color !== undefined) {this.color = properties.color;}
  8803. // A node is connected when it has a from and to node.
  8804. this.connect();
  8805. this.widthFixed = this.widthFixed || (properties.width !== undefined);
  8806. this.lengthFixed = this.lengthFixed || (properties.length !== undefined);
  8807. this.stiffness = 1 / this.length;
  8808. // set draw method based on style
  8809. switch (this.style) {
  8810. case 'line': this.draw = this._drawLine; break;
  8811. case 'arrow': this.draw = this._drawArrow; break;
  8812. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8813. case 'dash-line': this.draw = this._drawDashLine; break;
  8814. default: this.draw = this._drawLine; break;
  8815. }
  8816. };
  8817. /**
  8818. * Connect an edge to its nodes
  8819. */
  8820. Edge.prototype.connect = function () {
  8821. this.disconnect();
  8822. this.from = this.graph.nodes[this.fromId] || null;
  8823. this.to = this.graph.nodes[this.toId] || null;
  8824. this.connected = (this.from && this.to);
  8825. if (this.connected) {
  8826. this.from.attachEdge(this);
  8827. this.to.attachEdge(this);
  8828. }
  8829. else {
  8830. if (this.from) {
  8831. this.from.detachEdge(this);
  8832. }
  8833. if (this.to) {
  8834. this.to.detachEdge(this);
  8835. }
  8836. }
  8837. };
  8838. /**
  8839. * Disconnect an edge from its nodes
  8840. */
  8841. Edge.prototype.disconnect = function () {
  8842. if (this.from) {
  8843. this.from.detachEdge(this);
  8844. this.from = null;
  8845. }
  8846. if (this.to) {
  8847. this.to.detachEdge(this);
  8848. this.to = null;
  8849. }
  8850. this.connected = false;
  8851. };
  8852. /**
  8853. * get the title of this edge.
  8854. * @return {string} title The title of the edge, or undefined when no title
  8855. * has been set.
  8856. */
  8857. Edge.prototype.getTitle = function() {
  8858. return this.title;
  8859. };
  8860. /**
  8861. * Retrieve the value of the edge. Can be undefined
  8862. * @return {Number} value
  8863. */
  8864. Edge.prototype.getValue = function() {
  8865. return this.value;
  8866. };
  8867. /**
  8868. * Adjust the value range of the edge. The edge will adjust it's width
  8869. * based on its value.
  8870. * @param {Number} min
  8871. * @param {Number} max
  8872. */
  8873. Edge.prototype.setValueRange = function(min, max) {
  8874. if (!this.widthFixed && this.value !== undefined) {
  8875. var scale = (this.widthMax - this.widthMin) / (max - min);
  8876. this.width = (this.value - min) * scale + this.widthMin;
  8877. }
  8878. };
  8879. /**
  8880. * Redraw a edge
  8881. * Draw this edge in the given canvas
  8882. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8883. * @param {CanvasRenderingContext2D} ctx
  8884. */
  8885. Edge.prototype.draw = function(ctx) {
  8886. throw "Method draw not initialized in edge";
  8887. };
  8888. /**
  8889. * Check if this object is overlapping with the provided object
  8890. * @param {Object} obj an object with parameters left, top
  8891. * @return {boolean} True if location is located on the edge
  8892. */
  8893. Edge.prototype.isOverlappingWith = function(obj) {
  8894. var distMax = 10;
  8895. var xFrom = this.from.x;
  8896. var yFrom = this.from.y;
  8897. var xTo = this.to.x;
  8898. var yTo = this.to.y;
  8899. var xObj = obj.left;
  8900. var yObj = obj.top;
  8901. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8902. return (dist < distMax);
  8903. };
  8904. /**
  8905. * Redraw a edge as a line
  8906. * Draw this edge in the given canvas
  8907. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8908. * @param {CanvasRenderingContext2D} ctx
  8909. * @private
  8910. */
  8911. Edge.prototype._drawLine = function(ctx) {
  8912. // set style
  8913. ctx.strokeStyle = this.color;
  8914. ctx.lineWidth = this._getLineWidth();
  8915. var point;
  8916. if (this.from != this.to) {
  8917. // draw line
  8918. this._line(ctx);
  8919. // draw label
  8920. if (this.label) {
  8921. point = this._pointOnLine(0.5);
  8922. this._label(ctx, this.label, point.x, point.y);
  8923. }
  8924. }
  8925. else {
  8926. var x, y;
  8927. var radius = this.length / 4;
  8928. var node = this.from;
  8929. if (!node.width) {
  8930. node.resize(ctx);
  8931. }
  8932. if (node.width > node.height) {
  8933. x = node.x + node.width / 2;
  8934. y = node.y - radius;
  8935. }
  8936. else {
  8937. x = node.x + radius;
  8938. y = node.y - node.height / 2;
  8939. }
  8940. this._circle(ctx, x, y, radius);
  8941. point = this._pointOnCircle(x, y, radius, 0.5);
  8942. this._label(ctx, this.label, point.x, point.y);
  8943. }
  8944. };
  8945. /**
  8946. * Get the line width of the edge. Depends on width and whether one of the
  8947. * connected nodes is selected.
  8948. * @return {Number} width
  8949. * @private
  8950. */
  8951. Edge.prototype._getLineWidth = function() {
  8952. if (this.from.selected || this.to.selected) {
  8953. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  8954. }
  8955. else {
  8956. return this.width*this.graphScaleInv;
  8957. }
  8958. };
  8959. /**
  8960. * Draw a line between two nodes
  8961. * @param {CanvasRenderingContext2D} ctx
  8962. * @private
  8963. */
  8964. Edge.prototype._line = function (ctx) {
  8965. // draw a straight line
  8966. ctx.beginPath();
  8967. ctx.moveTo(this.from.x, this.from.y);
  8968. ctx.lineTo(this.to.x, this.to.y);
  8969. ctx.stroke();
  8970. };
  8971. /**
  8972. * Draw a line from a node to itself, a circle
  8973. * @param {CanvasRenderingContext2D} ctx
  8974. * @param {Number} x
  8975. * @param {Number} y
  8976. * @param {Number} radius
  8977. * @private
  8978. */
  8979. Edge.prototype._circle = function (ctx, x, y, radius) {
  8980. // draw a circle
  8981. ctx.beginPath();
  8982. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8983. ctx.stroke();
  8984. };
  8985. /**
  8986. * Draw label with white background and with the middle at (x, y)
  8987. * @param {CanvasRenderingContext2D} ctx
  8988. * @param {String} text
  8989. * @param {Number} x
  8990. * @param {Number} y
  8991. * @private
  8992. */
  8993. Edge.prototype._label = function (ctx, text, x, y) {
  8994. if (text) {
  8995. // TODO: cache the calculated size
  8996. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8997. this.fontSize + "px " + this.fontFace;
  8998. ctx.fillStyle = 'white';
  8999. var width = ctx.measureText(text).width;
  9000. var height = this.fontSize;
  9001. var left = x - width / 2;
  9002. var top = y - height / 2;
  9003. ctx.fillRect(left, top, width, height);
  9004. // draw text
  9005. ctx.fillStyle = this.fontColor || "black";
  9006. ctx.textAlign = "left";
  9007. ctx.textBaseline = "top";
  9008. ctx.fillText(text, left, top);
  9009. }
  9010. };
  9011. /**
  9012. * Redraw a edge as a dashed line
  9013. * Draw this edge in the given canvas
  9014. * @author David Jordan
  9015. * @date 2012-08-08
  9016. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9017. * @param {CanvasRenderingContext2D} ctx
  9018. * @private
  9019. */
  9020. Edge.prototype._drawDashLine = function(ctx) {
  9021. // set style
  9022. ctx.strokeStyle = this.color;
  9023. ctx.lineWidth = this._getLineWidth();
  9024. // draw dashed line
  9025. ctx.beginPath();
  9026. ctx.lineCap = 'round';
  9027. if (this.dash.altLength !== undefined) //If an alt dash value has been set add to the array this value
  9028. {
  9029. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9030. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  9031. }
  9032. 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
  9033. {
  9034. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  9035. [this.dash.length,this.dash.gap]);
  9036. }
  9037. else //If all else fails draw a line
  9038. {
  9039. ctx.moveTo(this.from.x, this.from.y);
  9040. ctx.lineTo(this.to.x, this.to.y);
  9041. }
  9042. ctx.stroke();
  9043. // draw label
  9044. if (this.label) {
  9045. var point = this._pointOnLine(0.5);
  9046. this._label(ctx, this.label, point.x, point.y);
  9047. }
  9048. };
  9049. /**
  9050. * Get a point on a line
  9051. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9052. * @return {Object} point
  9053. * @private
  9054. */
  9055. Edge.prototype._pointOnLine = function (percentage) {
  9056. return {
  9057. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  9058. y: (1 - percentage) * this.from.y + percentage * this.to.y
  9059. }
  9060. };
  9061. /**
  9062. * Get a point on a circle
  9063. * @param {Number} x
  9064. * @param {Number} y
  9065. * @param {Number} radius
  9066. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  9067. * @return {Object} point
  9068. * @private
  9069. */
  9070. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  9071. var angle = (percentage - 3/8) * 2 * Math.PI;
  9072. return {
  9073. x: x + radius * Math.cos(angle),
  9074. y: y - radius * Math.sin(angle)
  9075. }
  9076. };
  9077. /**
  9078. * Redraw a edge as a line with an arrow halfway the line
  9079. * Draw this edge in the given canvas
  9080. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9081. * @param {CanvasRenderingContext2D} ctx
  9082. * @private
  9083. */
  9084. Edge.prototype._drawArrowCenter = function(ctx) {
  9085. var point;
  9086. // set style
  9087. ctx.strokeStyle = this.color;
  9088. ctx.fillStyle = this.color;
  9089. ctx.lineWidth = this._getLineWidth();
  9090. if (this.from != this.to) {
  9091. // draw line
  9092. this._line(ctx);
  9093. // draw an arrow halfway the line
  9094. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9095. var length = 10 + 5 * this.width; // TODO: make customizable?
  9096. point = this._pointOnLine(0.5);
  9097. ctx.arrow(point.x, point.y, angle, length);
  9098. ctx.fill();
  9099. ctx.stroke();
  9100. // draw label
  9101. if (this.label) {
  9102. point = this._pointOnLine(0.5);
  9103. this._label(ctx, this.label, point.x, point.y);
  9104. }
  9105. }
  9106. else {
  9107. // draw circle
  9108. var x, y;
  9109. var radius = this.length / 4;
  9110. var node = this.from;
  9111. if (!node.width) {
  9112. node.resize(ctx);
  9113. }
  9114. if (node.width > node.height) {
  9115. x = node.x + node.width / 2;
  9116. y = node.y - radius;
  9117. }
  9118. else {
  9119. x = node.x + radius;
  9120. y = node.y - node.height / 2;
  9121. }
  9122. this._circle(ctx, x, y, radius);
  9123. // draw all arrows
  9124. var angle = 0.2 * Math.PI;
  9125. var length = 10 + 5 * this.width; // TODO: make customizable?
  9126. point = this._pointOnCircle(x, y, radius, 0.5);
  9127. ctx.arrow(point.x, point.y, angle, length);
  9128. ctx.fill();
  9129. ctx.stroke();
  9130. // draw label
  9131. if (this.label) {
  9132. point = this._pointOnCircle(x, y, radius, 0.5);
  9133. this._label(ctx, this.label, point.x, point.y);
  9134. }
  9135. }
  9136. };
  9137. /**
  9138. * Redraw a edge as a line with an arrow
  9139. * Draw this edge in the given canvas
  9140. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  9141. * @param {CanvasRenderingContext2D} ctx
  9142. * @private
  9143. */
  9144. Edge.prototype._drawArrow = function(ctx) {
  9145. // set style
  9146. ctx.strokeStyle = this.color;
  9147. ctx.fillStyle = this.color;
  9148. ctx.lineWidth = this._getLineWidth();
  9149. // draw line
  9150. var angle, length;
  9151. if (this.from != this.to) {
  9152. // calculate length and angle of the line
  9153. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  9154. var dx = (this.to.x - this.from.x);
  9155. var dy = (this.to.y - this.from.y);
  9156. var lEdge = Math.sqrt(dx * dx + dy * dy);
  9157. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  9158. var pFrom = (lEdge - lFrom) / lEdge;
  9159. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  9160. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  9161. var lTo = this.to.distanceToBorder(ctx, angle);
  9162. var pTo = (lEdge - lTo) / lEdge;
  9163. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  9164. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  9165. ctx.beginPath();
  9166. ctx.moveTo(xFrom, yFrom);
  9167. ctx.lineTo(xTo, yTo);
  9168. ctx.stroke();
  9169. // draw arrow at the end of the line
  9170. length = 10 + 5 * this.width; // TODO: make customizable?
  9171. ctx.arrow(xTo, yTo, angle, length);
  9172. ctx.fill();
  9173. ctx.stroke();
  9174. // draw label
  9175. if (this.label) {
  9176. var point = this._pointOnLine(0.5);
  9177. this._label(ctx, this.label, point.x, point.y);
  9178. }
  9179. }
  9180. else {
  9181. // draw circle
  9182. var node = this.from;
  9183. var x, y, arrow;
  9184. var radius = this.length / 4;
  9185. if (!node.width) {
  9186. node.resize(ctx);
  9187. }
  9188. if (node.width > node.height) {
  9189. x = node.x + node.width / 2;
  9190. y = node.y - radius;
  9191. arrow = {
  9192. x: x,
  9193. y: node.y,
  9194. angle: 0.9 * Math.PI
  9195. };
  9196. }
  9197. else {
  9198. x = node.x + radius;
  9199. y = node.y - node.height / 2;
  9200. arrow = {
  9201. x: node.x,
  9202. y: y,
  9203. angle: 0.6 * Math.PI
  9204. };
  9205. }
  9206. ctx.beginPath();
  9207. // TODO: do not draw a circle, but an arc
  9208. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9209. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9210. ctx.stroke();
  9211. // draw all arrows
  9212. length = 10 + 5 * this.width; // TODO: make customizable?
  9213. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9214. ctx.fill();
  9215. ctx.stroke();
  9216. // draw label
  9217. if (this.label) {
  9218. point = this._pointOnCircle(x, y, radius, 0.5);
  9219. this._label(ctx, this.label, point.x, point.y);
  9220. }
  9221. }
  9222. };
  9223. /**
  9224. * Calculate the distance between a point (x3,y3) and a line segment from
  9225. * (x1,y1) to (x2,y2).
  9226. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9227. * @param {number} x1
  9228. * @param {number} y1
  9229. * @param {number} x2
  9230. * @param {number} y2
  9231. * @param {number} x3
  9232. * @param {number} y3
  9233. * @private
  9234. */
  9235. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9236. var px = x2-x1,
  9237. py = y2-y1,
  9238. something = px*px + py*py,
  9239. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9240. if (u > 1) {
  9241. u = 1;
  9242. }
  9243. else if (u < 0) {
  9244. u = 0;
  9245. }
  9246. var x = x1 + u * px,
  9247. y = y1 + u * py,
  9248. dx = x - x3,
  9249. dy = y - y3;
  9250. //# Note: If the actual distance does not matter,
  9251. //# if you only want to compare what this function
  9252. //# returns to other results of this function, you
  9253. //# can just return the squared distance instead
  9254. //# (i.e. remove the sqrt) to gain a little performance
  9255. return Math.sqrt(dx*dx + dy*dy);
  9256. };
  9257. /**
  9258. * This allows the zoom level of the graph to influence the rendering
  9259. *
  9260. * @param scale
  9261. */
  9262. Edge.prototype.setScale = function(scale) {
  9263. this.graphScaleInv = 1.0/scale;
  9264. };
  9265. /**
  9266. * Popup is a class to create a popup window with some text
  9267. * @param {Element} container The container object.
  9268. * @param {Number} [x]
  9269. * @param {Number} [y]
  9270. * @param {String} [text]
  9271. */
  9272. function Popup(container, x, y, text) {
  9273. if (container) {
  9274. this.container = container;
  9275. }
  9276. else {
  9277. this.container = document.body;
  9278. }
  9279. this.x = 0;
  9280. this.y = 0;
  9281. this.padding = 5;
  9282. if (x !== undefined && y !== undefined ) {
  9283. this.setPosition(x, y);
  9284. }
  9285. if (text !== undefined) {
  9286. this.setText(text);
  9287. }
  9288. // create the frame
  9289. this.frame = document.createElement("div");
  9290. var style = this.frame.style;
  9291. style.position = "absolute";
  9292. style.visibility = "hidden";
  9293. style.border = "1px solid #666";
  9294. style.color = "black";
  9295. style.padding = this.padding + "px";
  9296. style.backgroundColor = "#FFFFC6";
  9297. style.borderRadius = "3px";
  9298. style.MozBorderRadius = "3px";
  9299. style.WebkitBorderRadius = "3px";
  9300. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  9301. style.whiteSpace = "nowrap";
  9302. this.container.appendChild(this.frame);
  9303. }
  9304. /**
  9305. * @param {number} x Horizontal position of the popup window
  9306. * @param {number} y Vertical position of the popup window
  9307. */
  9308. Popup.prototype.setPosition = function(x, y) {
  9309. this.x = parseInt(x);
  9310. this.y = parseInt(y);
  9311. };
  9312. /**
  9313. * Set the text for the popup window. This can be HTML code
  9314. * @param {string} text
  9315. */
  9316. Popup.prototype.setText = function(text) {
  9317. this.frame.innerHTML = text;
  9318. };
  9319. /**
  9320. * Show the popup window
  9321. * @param {boolean} show Optional. Show or hide the window
  9322. */
  9323. Popup.prototype.show = function (show) {
  9324. if (show === undefined) {
  9325. show = true;
  9326. }
  9327. if (show) {
  9328. var height = this.frame.clientHeight;
  9329. var width = this.frame.clientWidth;
  9330. var maxHeight = this.frame.parentNode.clientHeight;
  9331. var maxWidth = this.frame.parentNode.clientWidth;
  9332. var top = (this.y - height);
  9333. if (top + height + this.padding > maxHeight) {
  9334. top = maxHeight - height - this.padding;
  9335. }
  9336. if (top < this.padding) {
  9337. top = this.padding;
  9338. }
  9339. var left = this.x;
  9340. if (left + width + this.padding > maxWidth) {
  9341. left = maxWidth - width - this.padding;
  9342. }
  9343. if (left < this.padding) {
  9344. left = this.padding;
  9345. }
  9346. this.frame.style.left = left + "px";
  9347. this.frame.style.top = top + "px";
  9348. this.frame.style.visibility = "visible";
  9349. }
  9350. else {
  9351. this.hide();
  9352. }
  9353. };
  9354. /**
  9355. * Hide the popup window
  9356. */
  9357. Popup.prototype.hide = function () {
  9358. this.frame.style.visibility = "hidden";
  9359. };
  9360. /**
  9361. * @class Groups
  9362. * This class can store groups and properties specific for groups.
  9363. */
  9364. Groups = function () {
  9365. this.clear();
  9366. this.defaultIndex = 0;
  9367. };
  9368. /**
  9369. * default constants for group colors
  9370. */
  9371. Groups.DEFAULT = [
  9372. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  9373. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  9374. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  9375. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  9376. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  9377. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  9378. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  9379. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  9380. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  9381. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  9382. ];
  9383. /**
  9384. * Clear all groups
  9385. */
  9386. Groups.prototype.clear = function () {
  9387. this.groups = {};
  9388. this.groups.length = function()
  9389. {
  9390. var i = 0;
  9391. for ( var p in this ) {
  9392. if (this.hasOwnProperty(p)) {
  9393. i++;
  9394. }
  9395. }
  9396. return i;
  9397. }
  9398. };
  9399. /**
  9400. * get group properties of a groupname. If groupname is not found, a new group
  9401. * is added.
  9402. * @param {*} groupname Can be a number, string, Date, etc.
  9403. * @return {Object} group The created group, containing all group properties
  9404. */
  9405. Groups.prototype.get = function (groupname) {
  9406. var group = this.groups[groupname];
  9407. if (group == undefined) {
  9408. // create new group
  9409. var index = this.defaultIndex % Groups.DEFAULT.length;
  9410. this.defaultIndex++;
  9411. group = {};
  9412. group.color = Groups.DEFAULT[index];
  9413. this.groups[groupname] = group;
  9414. }
  9415. return group;
  9416. };
  9417. /**
  9418. * Add a custom group style
  9419. * @param {String} groupname
  9420. * @param {Object} style An object containing borderColor,
  9421. * backgroundColor, etc.
  9422. * @return {Object} group The created group object
  9423. */
  9424. Groups.prototype.add = function (groupname, style) {
  9425. this.groups[groupname] = style;
  9426. if (style.color) {
  9427. style.color = Node.parseColor(style.color);
  9428. }
  9429. return style;
  9430. };
  9431. /**
  9432. * @class Images
  9433. * This class loads images and keeps them stored.
  9434. */
  9435. Images = function () {
  9436. this.images = {};
  9437. this.callback = undefined;
  9438. };
  9439. /**
  9440. * Set an onload callback function. This will be called each time an image
  9441. * is loaded
  9442. * @param {function} callback
  9443. */
  9444. Images.prototype.setOnloadCallback = function(callback) {
  9445. this.callback = callback;
  9446. };
  9447. /**
  9448. *
  9449. * @param {string} url Url of the image
  9450. * @return {Image} img The image object
  9451. */
  9452. Images.prototype.load = function(url) {
  9453. var img = this.images[url];
  9454. if (img == undefined) {
  9455. // create the image
  9456. var images = this;
  9457. img = new Image();
  9458. this.images[url] = img;
  9459. img.onload = function() {
  9460. if (images.callback) {
  9461. images.callback(this);
  9462. }
  9463. };
  9464. img.src = url;
  9465. }
  9466. return img;
  9467. };
  9468. /**
  9469. * Creation of the SectorMixin var.
  9470. *
  9471. * This contains all the functions the Graph object can use to employ the sector system.
  9472. * The sector system is always used by Graph, though the benefits only apply to the use of clustering.
  9473. * If clustering is not used, there is no overhead except for a duplicate object with references to nodes and edges.
  9474. *
  9475. * Alex de Mulder
  9476. * 21-01-2013
  9477. */
  9478. var SectorMixin = {
  9479. /**
  9480. * This function is only called by the setData function of the Graph object.
  9481. * This loads the global references into the active sector. This initializes the sector.
  9482. *
  9483. * @private
  9484. */
  9485. _putDataInSector : function() {
  9486. this.sectors["active"][this._sector()].nodes = this.nodes;
  9487. this.sectors["active"][this._sector()].edges = this.edges;
  9488. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  9489. },
  9490. /**
  9491. * /**
  9492. * This function sets the global references to nodes, edges and nodeIndices back to
  9493. * those of the supplied (active) sector. If a type is defined, do the specific type
  9494. *
  9495. * @param {String} sectorId
  9496. * @param {String} [sectorType] | "active" or "frozen"
  9497. * @private
  9498. */
  9499. _switchToSector : function(sectorId, sectorType) {
  9500. if (sectorType === undefined || sectorType == "active") {
  9501. this._switchToActiveSector(sectorId);
  9502. }
  9503. else {
  9504. this._switchToFrozenSector(sectorId);
  9505. }
  9506. },
  9507. /**
  9508. * This function sets the global references to nodes, edges and nodeIndices back to
  9509. * those of the supplied active sector.
  9510. *
  9511. * @param sectorId
  9512. * @private
  9513. */
  9514. _switchToActiveSector : function(sectorId) {
  9515. this.nodeIndices = this.sectors["active"][sectorId]["nodeIndices"];
  9516. this.nodes = this.sectors["active"][sectorId]["nodes"];
  9517. this.edges = this.sectors["active"][sectorId]["edges"];
  9518. },
  9519. /**
  9520. * This function sets the global references to nodes, edges and nodeIndices back to
  9521. * those of the supplied frozen sector.
  9522. *
  9523. * @param sectorId
  9524. * @private
  9525. */
  9526. _switchToFrozenSector : function(sectorId) {
  9527. this.nodeIndices = this.sectors["frozen"][sectorId]["nodeIndices"];
  9528. this.nodes = this.sectors["frozen"][sectorId]["nodes"];
  9529. this.edges = this.sectors["frozen"][sectorId]["edges"];
  9530. },
  9531. /**
  9532. * This function sets the global references to nodes, edges and nodeIndices to
  9533. * those of the navigation controls sector.
  9534. *
  9535. * @private
  9536. */
  9537. _switchToNavigationSector : function() {
  9538. this.nodeIndices = this.sectors["navigation"]["nodeIndices"];
  9539. this.nodes = this.sectors["navigation"]["nodes"];
  9540. this.edges = this.sectors["navigation"]["edges"];
  9541. },
  9542. /**
  9543. * This function sets the global references to nodes, edges and nodeIndices back to
  9544. * those of the currently active sector.
  9545. *
  9546. * @private
  9547. */
  9548. _loadLatestSector : function() {
  9549. this._switchToSector(this._sector());
  9550. },
  9551. /**
  9552. * This function returns the currently active sector Id
  9553. *
  9554. * @returns {String}
  9555. * @private
  9556. */
  9557. _sector : function() {
  9558. return this.activeSector[this.activeSector.length-1];
  9559. },
  9560. /**
  9561. * This function returns the previously active sector Id
  9562. *
  9563. * @returns {String}
  9564. * @private
  9565. */
  9566. _previousSector : function() {
  9567. if (this.activeSector.length > 1) {
  9568. return this.activeSector[this.activeSector.length-2];
  9569. }
  9570. else {
  9571. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  9572. }
  9573. },
  9574. /**
  9575. * We add the active sector at the end of the this.activeSector array
  9576. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  9577. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  9578. *
  9579. * @param newId
  9580. * @private
  9581. */
  9582. _setActiveSector : function(newId) {
  9583. this.activeSector.push(newId);
  9584. },
  9585. /**
  9586. * We remove the currently active sector id from the active sector stack. This happens when
  9587. * we reactivate the previously active sector
  9588. *
  9589. * @private
  9590. */
  9591. _forgetLastSector : function() {
  9592. this.activeSector.pop();
  9593. },
  9594. /**
  9595. * This function creates a new active sector with the supplied newId. This newId
  9596. * is the expanding node id.
  9597. *
  9598. * @param {String} newId | Id of the new active sector
  9599. * @private
  9600. */
  9601. _createNewSector : function(newId) {
  9602. // create the new sector
  9603. this.sectors["active"][newId] = {"nodes":{},
  9604. "edges":{},
  9605. "nodeIndices":[],
  9606. "formationScale": this.scale,
  9607. "drawingNode": undefined};
  9608. // create the new sector render node. This gives visual feedback that you are in a new sector.
  9609. this.sectors["active"][newId]['drawingNode'] = new Node(
  9610. {id:newId,
  9611. color: {
  9612. background: "#eaefef",
  9613. border: "495c5e"
  9614. }
  9615. },{},{},this.constants);
  9616. this.sectors["active"][newId]['drawingNode'].clusterSize = 2;
  9617. },
  9618. /**
  9619. * This function removes the currently active sector. This is called when we create a new
  9620. * active sector.
  9621. *
  9622. * @param {String} sectorId | Id of the active sector that will be removed
  9623. * @private
  9624. */
  9625. _deleteActiveSector : function(sectorId) {
  9626. delete this.sectors["active"][sectorId];
  9627. },
  9628. /**
  9629. * This function removes the currently active sector. This is called when we reactivate
  9630. * the previously active sector.
  9631. *
  9632. * @param {String} sectorId | Id of the active sector that will be removed
  9633. * @private
  9634. */
  9635. _deleteFrozenSector : function(sectorId) {
  9636. delete this.sectors["frozen"][sectorId];
  9637. },
  9638. /**
  9639. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  9640. * We copy the references, then delete the active entree.
  9641. *
  9642. * @param sectorId
  9643. * @private
  9644. */
  9645. _freezeSector : function(sectorId) {
  9646. // we move the set references from the active to the frozen stack.
  9647. this.sectors["frozen"][sectorId] = this.sectors["active"][sectorId];
  9648. // we have moved the sector data into the frozen set, we now remove it from the active set
  9649. this._deleteActiveSector(sectorId);
  9650. },
  9651. /**
  9652. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  9653. * object to the "active" object.
  9654. *
  9655. * @param sectorId
  9656. * @private
  9657. */
  9658. _activateSector : function(sectorId) {
  9659. // we move the set references from the frozen to the active stack.
  9660. this.sectors["active"][sectorId] = this.sectors["frozen"][sectorId];
  9661. // we have moved the sector data into the active set, we now remove it from the frozen stack
  9662. this._deleteFrozenSector(sectorId);
  9663. },
  9664. /**
  9665. * This function merges the data from the currently active sector with a frozen sector. This is used
  9666. * in the process of reverting back to the previously active sector.
  9667. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  9668. * upon the creation of a new active sector.
  9669. *
  9670. * @param sectorId
  9671. * @private
  9672. */
  9673. _mergeThisWithFrozen : function(sectorId) {
  9674. // copy all nodes
  9675. for (var nodeId in this.nodes) {
  9676. if (this.nodes.hasOwnProperty(nodeId)) {
  9677. this.sectors["frozen"][sectorId]["nodes"][nodeId] = this.nodes[nodeId];
  9678. }
  9679. }
  9680. // copy all edges (if not fully clustered, else there are no edges)
  9681. for (var edgeId in this.edges) {
  9682. if (this.edges.hasOwnProperty(edgeId)) {
  9683. this.sectors["frozen"][sectorId]["edges"][edgeId] = this.edges[edgeId];
  9684. }
  9685. }
  9686. // merge the nodeIndices
  9687. for (var i = 0; i < this.nodeIndices.length; i++) {
  9688. this.sectors["frozen"][sectorId]["nodeIndices"].push(this.nodeIndices[i]);
  9689. }
  9690. },
  9691. /**
  9692. * This clusters the sector to one cluster. It was a single cluster before this process started so
  9693. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  9694. *
  9695. * @private
  9696. */
  9697. _collapseThisToSingleCluster : function() {
  9698. this.clusterToFit(1,false);
  9699. },
  9700. /**
  9701. * We create a new active sector from the node that we want to open.
  9702. *
  9703. * @param node
  9704. * @private
  9705. */
  9706. _addSector : function(node) {
  9707. // this is the currently active sector
  9708. var sector = this._sector();
  9709. // // this should allow me to select nodes from a frozen set.
  9710. // if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  9711. // console.log("the node is part of the active sector");
  9712. // }
  9713. // else {
  9714. // console.log("I dont know what the fuck happened!!");
  9715. // }
  9716. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  9717. delete this.nodes[node.id];
  9718. var unqiueIdentifier = util.randomUUID();
  9719. // we fully freeze the currently active sector
  9720. this._freezeSector(sector);
  9721. // we create a new active sector. This sector has the Id of the node to ensure uniqueness
  9722. this._createNewSector(unqiueIdentifier);
  9723. // we add the active sector to the sectors array to be able to revert these steps later on
  9724. this._setActiveSector(unqiueIdentifier);
  9725. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  9726. this._switchToSector(this._sector());
  9727. // finally we add the node we removed from our previous active sector to the new active sector
  9728. this.nodes[node.id] = node;
  9729. },
  9730. /**
  9731. * We close the sector that is currently open and revert back to the one before.
  9732. * If the active sector is the "default" sector, nothing happens.
  9733. *
  9734. * @private
  9735. */
  9736. _collapseSector : function() {
  9737. // the currently active sector
  9738. var sector = this._sector();
  9739. // we cannot collapse the default sector
  9740. if (sector != "default") {
  9741. if ((this.nodeIndices.length == 1) ||
  9742. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  9743. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  9744. var previousSector = this._previousSector();
  9745. // we collapse the sector back to a single cluster
  9746. this._collapseThisToSingleCluster();
  9747. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  9748. // This previous sector is the one we will reactivate
  9749. this._mergeThisWithFrozen(previousSector);
  9750. // the previously active (frozen) sector now has all the data from the currently active sector.
  9751. // we can now delete the active sector.
  9752. this._deleteActiveSector(sector);
  9753. // we activate the previously active (and currently frozen) sector.
  9754. this._activateSector(previousSector);
  9755. // we load the references from the newly active sector into the global references
  9756. this._switchToSector(previousSector);
  9757. // we forget the previously active sector because we reverted to the one before
  9758. this._forgetLastSector();
  9759. // finally, we update the node index list.
  9760. this._updateNodeIndexList();
  9761. }
  9762. }
  9763. },
  9764. /**
  9765. * This runs a function in all active sectors. This is used in _redraw() and the _initializeForceCalculation().
  9766. *
  9767. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  9768. * | we dont pass the function itself because then the "this" is the window object
  9769. * | instead of the Graph object
  9770. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  9771. * @private
  9772. */
  9773. _doInAllActiveSectors : function(runFunction,argument) {
  9774. if (argument === undefined) {
  9775. for (var sector in this.sectors["active"]) {
  9776. if (this.sectors["active"].hasOwnProperty(sector)) {
  9777. // switch the global references to those of this sector
  9778. this._switchToActiveSector(sector);
  9779. this[runFunction]();
  9780. }
  9781. }
  9782. }
  9783. else {
  9784. for (var sector in this.sectors["active"]) {
  9785. if (this.sectors["active"].hasOwnProperty(sector)) {
  9786. // switch the global references to those of this sector
  9787. this._switchToActiveSector(sector);
  9788. var args = Array.prototype.splice.call(arguments, 1);
  9789. if (args.length > 1) {
  9790. this[runFunction](args[0],args[1]);
  9791. }
  9792. else {
  9793. this[runFunction](argument);
  9794. }
  9795. }
  9796. }
  9797. }
  9798. // we revert the global references back to our active sector
  9799. this._loadLatestSector();
  9800. },
  9801. /**
  9802. * This runs a function in all frozen sectors. This is used in the _redraw().
  9803. *
  9804. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  9805. * | we don't pass the function itself because then the "this" is the window object
  9806. * | instead of the Graph object
  9807. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  9808. * @private
  9809. */
  9810. _doInAllFrozenSectors : function(runFunction,argument) {
  9811. if (argument === undefined) {
  9812. for (var sector in this.sectors["frozen"]) {
  9813. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  9814. // switch the global references to those of this sector
  9815. this._switchToFrozenSector(sector);
  9816. this[runFunction]();
  9817. }
  9818. }
  9819. }
  9820. else {
  9821. for (var sector in this.sectors["frozen"]) {
  9822. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  9823. // switch the global references to those of this sector
  9824. this._switchToFrozenSector(sector);
  9825. var args = Array.prototype.splice.call(arguments, 1);
  9826. if (args.length > 1) {
  9827. this[runFunction](args[0],args[1]);
  9828. }
  9829. else {
  9830. this[runFunction](argument);
  9831. }
  9832. }
  9833. }
  9834. }
  9835. this._loadLatestSector();
  9836. },
  9837. /**
  9838. * This runs a function in the navigation controls sector.
  9839. *
  9840. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  9841. * | we don't pass the function itself because then the "this" is the window object
  9842. * | instead of the Graph object
  9843. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  9844. * @private
  9845. */
  9846. _doInNavigationSector : function(runFunction,argument) {
  9847. this._switchToNavigationSector();
  9848. if (argument === undefined) {
  9849. this[runFunction]();
  9850. }
  9851. else {
  9852. var args = Array.prototype.splice.call(arguments, 1);
  9853. if (args.length > 1) {
  9854. this[runFunction](args[0],args[1]);
  9855. }
  9856. else {
  9857. this[runFunction](argument);
  9858. }
  9859. }
  9860. this._loadLatestSector();
  9861. },
  9862. /**
  9863. * This runs a function in all sectors. This is used in the _redraw().
  9864. *
  9865. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  9866. * | we don't pass the function itself because then the "this" is the window object
  9867. * | instead of the Graph object
  9868. * @param {*} [argument] | Optional: arguments to pass to the runFunction
  9869. * @private
  9870. */
  9871. _doInAllSectors : function(runFunction,argument) {
  9872. var args = Array.prototype.splice.call(arguments, 1);
  9873. if (argument === undefined) {
  9874. this._doInAllActiveSectors(runFunction);
  9875. this._doInAllFrozenSectors(runFunction);
  9876. }
  9877. else {
  9878. if (args.length > 1) {
  9879. this._doInAllActiveSectors(runFunction,args[0],args[1]);
  9880. this._doInAllFrozenSectors(runFunction,args[0],args[1]);
  9881. }
  9882. else {
  9883. this._doInAllActiveSectors(runFunction,argument);
  9884. this._doInAllFrozenSectors(runFunction,argument);
  9885. }
  9886. }
  9887. },
  9888. /**
  9889. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  9890. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  9891. *
  9892. * @private
  9893. */
  9894. _clearNodeIndexList : function() {
  9895. var sector = this._sector();
  9896. this.sectors["active"][sector]["nodeIndices"] = [];
  9897. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  9898. },
  9899. /**
  9900. * Draw the encompassing sector node
  9901. *
  9902. * @param ctx
  9903. * @param sectorType
  9904. * @private
  9905. */
  9906. _drawSectorNodes : function(ctx,sectorType) {
  9907. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  9908. for (var sector in this.sectors[sectorType]) {
  9909. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  9910. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  9911. this._switchToSector(sector,sectorType);
  9912. minY = 1e9; maxY = -1e9; minX = 1e9; maxX = -1e9;
  9913. for (var nodeId in this.nodes) {
  9914. if (this.nodes.hasOwnProperty(nodeId)) {
  9915. node = this.nodes[nodeId];
  9916. node.resize(ctx);
  9917. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  9918. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  9919. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  9920. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  9921. }
  9922. }
  9923. node = this.sectors[sectorType][sector]["drawingNode"];
  9924. node.x = 0.5 * (maxX + minX);
  9925. node.y = 0.5 * (maxY + minY);
  9926. node.width = 2 * (node.x - minX);
  9927. node.height = 2 * (node.y - minY);
  9928. node.radius = Math.sqrt(Math.pow(0.5*node.width,2) + Math.pow(0.5*node.height,2));
  9929. node.setScale(this.scale);
  9930. node._drawCircle(ctx);
  9931. }
  9932. }
  9933. }
  9934. },
  9935. _drawAllSectorNodes : function(ctx) {
  9936. this._drawSectorNodes(ctx,"frozen");
  9937. this._drawSectorNodes(ctx,"active");
  9938. this._loadLatestSector();
  9939. }
  9940. };
  9941. /**
  9942. * Creation of the ClusterMixin var.
  9943. *
  9944. * This contains all the functions the Graph object can use to employ clustering
  9945. *
  9946. * Alex de Mulder
  9947. * 21-01-2013
  9948. */
  9949. var ClusterMixin = {
  9950. /**
  9951. * This is only called in the constructor of the graph object
  9952. * */
  9953. startWithClustering : function() {
  9954. // cluster if the data set is big
  9955. this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
  9956. // updates the lables after clustering
  9957. this.updateLabels();
  9958. // this is called here because if clusterin is disabled, the start and stabilize are called in
  9959. // the setData function.
  9960. if (this.stabilize) {
  9961. this._doStabilize();
  9962. }
  9963. this.start();
  9964. },
  9965. /**
  9966. * This function clusters until the initialMaxNodes has been reached
  9967. *
  9968. * @param {Number} maxNumberOfNodes
  9969. * @param {Boolean} reposition
  9970. */
  9971. clusterToFit : function(maxNumberOfNodes, reposition) {
  9972. var numberOfNodes = this.nodeIndices.length;
  9973. var maxLevels = 50;
  9974. var level = 0;
  9975. // we first cluster the hubs, then we pull in the outliers, repeat
  9976. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  9977. if (level % 3 == 0) {
  9978. this.forceAggregateHubs();
  9979. }
  9980. else {
  9981. this.increaseClusterLevel();
  9982. }
  9983. numberOfNodes = this.nodeIndices.length;
  9984. level += 1;
  9985. }
  9986. // after the clustering we reposition the nodes to reduce the initial chaos
  9987. if (level > 1 && reposition == true) {
  9988. this.repositionNodes();
  9989. }
  9990. },
  9991. /**
  9992. * This function can be called to open up a specific cluster. It is only called by
  9993. * It will unpack the cluster back one level.
  9994. *
  9995. * @param node | Node object: cluster to open.
  9996. */
  9997. openCluster : function(node) {
  9998. var isMovingBeforeClustering = this.moving;
  9999. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
  10000. !(this._sector() == "default" && this.nodeIndices.length == 1)) {
  10001. this._addSector(node);
  10002. var level = 0;
  10003. while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
  10004. this.decreaseClusterLevel();
  10005. level += 1;
  10006. }
  10007. }
  10008. else {
  10009. this._expandClusterNode(node,false,true);
  10010. // update the index list, dynamic edges and labels
  10011. this._updateNodeIndexList();
  10012. this._updateDynamicEdges();
  10013. this.updateLabels();
  10014. }
  10015. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  10016. if (this.moving != isMovingBeforeClustering) {
  10017. this.start();
  10018. }
  10019. },
  10020. /**
  10021. * This calls the updateClustes with default arguments
  10022. */
  10023. updateClustersDefault : function() {
  10024. if (this.constants.clustering.enabled == true) {
  10025. this.updateClusters(0,false,false);
  10026. }
  10027. },
  10028. /**
  10029. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  10030. * be clustered with their connected node. This can be repeated as many times as needed.
  10031. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  10032. */
  10033. increaseClusterLevel : function() {
  10034. this.updateClusters(-1,false,true);
  10035. },
  10036. /**
  10037. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  10038. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  10039. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  10040. */
  10041. decreaseClusterLevel : function() {
  10042. this.updateClusters(1,false,true);
  10043. },
  10044. /**
  10045. * This is the main clustering function. It clusters and declusters on zoom or forced
  10046. * This function clusters on zoom, it can be called with a predefined zoom direction
  10047. * If out, check if we can form clusters, if in, check if we can open clusters.
  10048. * This function is only called from _zoom()
  10049. *
  10050. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  10051. * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
  10052. * @param {Boolean} force | enabled or disable forcing
  10053. *
  10054. */
  10055. updateClusters : function(zoomDirection,recursive,force) {
  10056. var isMovingBeforeClustering = this.moving;
  10057. var amountOfNodes = this.nodeIndices.length;
  10058. // on zoom out collapse the sector if the scale is at the level the sector was made
  10059. if (this.previousScale > this.scale && zoomDirection == 0) {
  10060. this._collapseSector();
  10061. }
  10062. // check if we zoom in or out
  10063. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  10064. // forming clusters when forced pulls outliers in. When not forced, the edge length of the
  10065. // outer nodes determines if it is being clustered
  10066. this._formClusters(force);
  10067. }
  10068. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  10069. if (force == true) {
  10070. // _openClusters checks for each node if the formationScale of the cluster is smaller than
  10071. // the current scale and if so, declusters. When forced, all clusters are reduced by one step
  10072. this._openClusters(recursive,force);
  10073. }
  10074. else {
  10075. // if a cluster takes up a set percentage of the active window
  10076. this._openClustersBySize();
  10077. }
  10078. }
  10079. this._updateNodeIndexList();
  10080. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  10081. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  10082. this._aggregateHubs(force);
  10083. this._updateNodeIndexList();
  10084. }
  10085. // we now reduce chains.
  10086. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  10087. this.handleChains();
  10088. this._updateNodeIndexList();
  10089. }
  10090. this.previousScale = this.scale;
  10091. // rest of the update the index list, dynamic edges and labels
  10092. this._updateDynamicEdges();
  10093. this.updateLabels();
  10094. // if a cluster was formed, we increase the clusterSession
  10095. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  10096. this.clusterSession += 1;
  10097. }
  10098. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  10099. if (this.moving != isMovingBeforeClustering) {
  10100. this.start();
  10101. }
  10102. },
  10103. /**
  10104. * This function handles the chains. It is called on every updateClusters().
  10105. */
  10106. handleChains : function() {
  10107. // after clustering we check how many chains there are
  10108. var chainPercentage = this._getChainFraction();
  10109. if (chainPercentage > this.constants.clustering.chainThreshold) {
  10110. this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
  10111. }
  10112. },
  10113. /**
  10114. * this functions starts clustering by hubs
  10115. * The minimum hub threshold is set globally
  10116. *
  10117. * @private
  10118. */
  10119. _aggregateHubs : function(force) {
  10120. this._getHubSize();
  10121. this._formClustersByHub(force,false);
  10122. },
  10123. /**
  10124. * This function is fired by keypress. It forces hubs to form.
  10125. *
  10126. */
  10127. forceAggregateHubs : function() {
  10128. var isMovingBeforeClustering = this.moving;
  10129. var amountOfNodes = this.nodeIndices.length;
  10130. this._aggregateHubs(true);
  10131. // update the index list, dynamic edges and labels
  10132. this._updateNodeIndexList();
  10133. this._updateDynamicEdges();
  10134. this.updateLabels();
  10135. // if a cluster was formed, we increase the clusterSession
  10136. if (this.nodeIndices.length != amountOfNodes) {
  10137. this.clusterSession += 1;
  10138. }
  10139. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  10140. if (this.moving != isMovingBeforeClustering) {
  10141. this.start();
  10142. }
  10143. },
  10144. /**
  10145. * If a cluster takes up more than a set percentage of the screen, open the cluster
  10146. *
  10147. * @private
  10148. */
  10149. _openClustersBySize : function() {
  10150. for (var nodeId in this.nodes) {
  10151. if (this.nodes.hasOwnProperty(nodeId)) {
  10152. var node = this.nodes[nodeId];
  10153. if (node.inView() == true) {
  10154. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  10155. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  10156. this.openCluster(node);
  10157. }
  10158. }
  10159. }
  10160. }
  10161. },
  10162. /**
  10163. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  10164. * has to be opened based on the current zoom level.
  10165. *
  10166. * @private
  10167. */
  10168. _openClusters : function(recursive,force) {
  10169. for (var i = 0; i < this.nodeIndices.length; i++) {
  10170. var node = this.nodes[this.nodeIndices[i]];
  10171. this._expandClusterNode(node,recursive,force);
  10172. }
  10173. },
  10174. /**
  10175. * This function checks if a node has to be opened. This is done by checking the zoom level.
  10176. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  10177. * This recursive behaviour is optional and can be set by the recursive argument.
  10178. *
  10179. * @param {Node} parentNode | to check for cluster and expand
  10180. * @param {Boolean} recursive | enabled or disable recursive calling
  10181. * @param {Boolean} force | enabled or disable forcing
  10182. * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
  10183. * @private
  10184. */
  10185. _expandClusterNode : function(parentNode, recursive, force, openAll) {
  10186. // first check if node is a cluster
  10187. if (parentNode.clusterSize > 1) {
  10188. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  10189. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  10190. openAll = true;
  10191. }
  10192. recursive = openAll ? true : recursive;
  10193. // if the last child has been added on a smaller scale than current scale decluster
  10194. if (parentNode.formationScale < this.scale || force == true) {
  10195. // we will check if any of the contained child nodes should be removed from the cluster
  10196. for (var containedNodeId in parentNode.containedNodes) {
  10197. if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
  10198. var childNode = parentNode.containedNodes[containedNodeId];
  10199. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  10200. // the largest cluster is the one that comes from outside
  10201. if (force == true) {
  10202. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  10203. || openAll) {
  10204. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  10205. }
  10206. }
  10207. else {
  10208. if (this._nodeInActiveArea(parentNode)) {
  10209. this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
  10210. }
  10211. }
  10212. }
  10213. }
  10214. }
  10215. }
  10216. },
  10217. /**
  10218. * ONLY CALLED FROM _expandClusterNode
  10219. *
  10220. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  10221. * the child node from the parent contained_node object and put it back into the global nodes object.
  10222. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  10223. *
  10224. * @param {Node} parentNode | the parent node
  10225. * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
  10226. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  10227. * With force and recursive both true, the entire cluster is unpacked
  10228. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  10229. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  10230. * @private
  10231. */
  10232. _expelChildFromParent : function(parentNode, containedNodeId, recursive, force, openAll) {
  10233. var childNode = parentNode.containedNodes[containedNodeId];
  10234. // if child node has been added on smaller scale than current, kick out
  10235. if (childNode.formationScale < this.scale || force == true) {
  10236. // put the child node back in the global nodes object
  10237. this.nodes[containedNodeId] = childNode;
  10238. // release the contained edges from this childNode back into the global edges
  10239. this._releaseContainedEdges(parentNode,childNode);
  10240. // reconnect rerouted edges to the childNode
  10241. this._connectEdgeBackToChild(parentNode,childNode);
  10242. // validate all edges in dynamicEdges
  10243. this._validateEdges(parentNode);
  10244. // undo the changes from the clustering operation on the parent node
  10245. parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
  10246. parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
  10247. parentNode.clusterSize -= childNode.clusterSize;
  10248. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  10249. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  10250. childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
  10251. childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
  10252. // remove node from the list
  10253. delete parentNode.containedNodes[containedNodeId];
  10254. // check if there are other childs with this clusterSession in the parent.
  10255. var othersPresent = false;
  10256. for (var childNodeId in parentNode.containedNodes) {
  10257. if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
  10258. if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
  10259. othersPresent = true;
  10260. break;
  10261. }
  10262. }
  10263. }
  10264. // if there are no others, remove the cluster session from the list
  10265. if (othersPresent == false) {
  10266. parentNode.clusterSessions.pop();
  10267. }
  10268. // remove the clusterSession from the child node
  10269. childNode.clusterSession = 0;
  10270. // restart the simulation to reorganise all nodes
  10271. this.moving = true;
  10272. // recalculate the size of the node on the next time the node is rendered
  10273. parentNode.clearSizeCache();
  10274. }
  10275. // check if a further expansion step is possible if recursivity is enabled
  10276. if (recursive == true) {
  10277. this._expandClusterNode(childNode,recursive,force,openAll);
  10278. }
  10279. },
  10280. /**
  10281. * This function checks if any nodes at the end of their trees have edges below a threshold length
  10282. * This function is called only from updateClusters()
  10283. * forceLevelCollapse ignores the length of the edge and collapses one level
  10284. * This means that a node with only one edge will be clustered with its connected node
  10285. *
  10286. * @private
  10287. * @param {Boolean} force
  10288. */
  10289. _formClusters : function(force) {
  10290. if (force == false) {
  10291. this._formClustersByZoom();
  10292. }
  10293. else {
  10294. this._forceClustersByZoom();
  10295. }
  10296. },
  10297. /**
  10298. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  10299. *
  10300. * @private
  10301. */
  10302. _formClustersByZoom : function() {
  10303. var dx,dy,length,
  10304. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  10305. // check if any edges are shorter than minLength and start the clustering
  10306. // the clustering favours the node with the larger mass
  10307. for (var edgeId in this.edges) {
  10308. if (this.edges.hasOwnProperty(edgeId)) {
  10309. var edge = this.edges[edgeId];
  10310. if (edge.connected) {
  10311. if (edge.toId != edge.fromId) {
  10312. dx = (edge.to.x - edge.from.x);
  10313. dy = (edge.to.y - edge.from.y);
  10314. length = Math.sqrt(dx * dx + dy * dy);
  10315. if (length < minLength) {
  10316. // first check which node is larger
  10317. var parentNode = edge.from;
  10318. var childNode = edge.to;
  10319. if (edge.to.mass > edge.from.mass) {
  10320. parentNode = edge.to;
  10321. childNode = edge.from;
  10322. }
  10323. if (childNode.dynamicEdgesLength == 1) {
  10324. this._addToCluster(parentNode,childNode,false);
  10325. }
  10326. else if (parentNode.dynamicEdgesLength == 1) {
  10327. this._addToCluster(childNode,parentNode,false);
  10328. }
  10329. }
  10330. }
  10331. }
  10332. }
  10333. }
  10334. },
  10335. /**
  10336. * This function forces the graph to cluster all nodes with only one connecting edge to their
  10337. * connected node.
  10338. *
  10339. * @private
  10340. */
  10341. _forceClustersByZoom : function() {
  10342. for (var nodeId in this.nodes) {
  10343. // another node could have absorbed this child.
  10344. if (this.nodes.hasOwnProperty(nodeId)) {
  10345. var childNode = this.nodes[nodeId];
  10346. // the edges can be swallowed by another decrease
  10347. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  10348. var edge = childNode.dynamicEdges[0];
  10349. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  10350. // group to the largest node
  10351. if (childNode.id != parentNode.id) {
  10352. if (parentNode.mass > childNode.mass) {
  10353. this._addToCluster(parentNode,childNode,true);
  10354. }
  10355. else {
  10356. this._addToCluster(childNode,parentNode,true);
  10357. }
  10358. }
  10359. }
  10360. }
  10361. }
  10362. },
  10363. /**
  10364. * This function forms clusters from hubs, it loops over all nodes
  10365. *
  10366. * @param {Boolean} force | Disregard zoom level
  10367. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  10368. * @private
  10369. */
  10370. _formClustersByHub : function(force, onlyEqual) {
  10371. // we loop over all nodes in the list
  10372. for (var nodeId in this.nodes) {
  10373. // we check if it is still available since it can be used by the clustering in this loop
  10374. if (this.nodes.hasOwnProperty(nodeId)) {
  10375. this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
  10376. }
  10377. }
  10378. },
  10379. /**
  10380. * This function forms a cluster from a specific preselected hub node
  10381. *
  10382. * @param {Node} hubNode | the node we will cluster as a hub
  10383. * @param {Boolean} force | Disregard zoom level
  10384. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  10385. * @param {Number} [absorptionSizeOffset] |
  10386. * @private
  10387. */
  10388. _formClusterFromHub : function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  10389. if (absorptionSizeOffset === undefined) {
  10390. absorptionSizeOffset = 0;
  10391. }
  10392. // we decide if the node is a hub
  10393. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  10394. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  10395. // initialize variables
  10396. var dx,dy,length;
  10397. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  10398. var allowCluster = false;
  10399. // we create a list of edges because the dynamicEdges change over the course of this loop
  10400. var edgesIdarray = [];
  10401. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  10402. for (var j = 0; j < amountOfInitialEdges; j++) {
  10403. edgesIdarray.push(hubNode.dynamicEdges[j].id);
  10404. }
  10405. // if the hub clustering is not forces, we check if one of the edges connected
  10406. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  10407. if (force == false) {
  10408. allowCluster = false;
  10409. for (j = 0; j < amountOfInitialEdges; j++) {
  10410. var edge = this.edges[edgesIdarray[j]];
  10411. if (edge !== undefined) {
  10412. if (edge.connected) {
  10413. if (edge.toId != edge.fromId) {
  10414. dx = (edge.to.x - edge.from.x);
  10415. dy = (edge.to.y - edge.from.y);
  10416. length = Math.sqrt(dx * dx + dy * dy);
  10417. if (length < minLength) {
  10418. allowCluster = true;
  10419. break;
  10420. }
  10421. }
  10422. }
  10423. }
  10424. }
  10425. }
  10426. // start the clustering if allowed
  10427. if ((!force && allowCluster) || force) {
  10428. // we loop over all edges INITIALLY connected to this hub
  10429. for (j = 0; j < amountOfInitialEdges; j++) {
  10430. edge = this.edges[edgesIdarray[j]];
  10431. // the edge can be clustered by this function in a previous loop
  10432. if (edge !== undefined) {
  10433. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  10434. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  10435. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  10436. (childNode.id != hubNode.id)) {
  10437. this._addToCluster(hubNode,childNode,force);
  10438. }
  10439. }
  10440. }
  10441. }
  10442. }
  10443. },
  10444. /**
  10445. * This function adds the child node to the parent node, creating a cluster if it is not already.
  10446. *
  10447. * @param {Node} parentNode | this is the node that will house the child node
  10448. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  10449. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  10450. * @private
  10451. */
  10452. _addToCluster : function(parentNode, childNode, force) {
  10453. // join child node in the parent node
  10454. parentNode.containedNodes[childNode.id] = childNode;
  10455. // manage all the edges connected to the child and parent nodes
  10456. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  10457. var edge = childNode.dynamicEdges[i];
  10458. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  10459. this._addToContainedEdges(parentNode,childNode,edge);
  10460. }
  10461. else {
  10462. this._connectEdgeToCluster(parentNode,childNode,edge);
  10463. }
  10464. }
  10465. // a contained node has no dynamic edges.
  10466. childNode.dynamicEdges = [];
  10467. // remove circular edges from clusters
  10468. this._containCircularEdgesFromNode(parentNode,childNode);
  10469. // remove the childNode from the global nodes object
  10470. delete this.nodes[childNode.id];
  10471. // update the properties of the child and parent
  10472. var massBefore = parentNode.mass;
  10473. childNode.clusterSession = this.clusterSession;
  10474. parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
  10475. parentNode.clusterSize += childNode.clusterSize;
  10476. parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
  10477. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  10478. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  10479. parentNode.clusterSessions.push(this.clusterSession);
  10480. }
  10481. // forced clusters only open from screen size and double tap
  10482. if (force == true) {
  10483. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  10484. parentNode.formationScale = 0;
  10485. }
  10486. else {
  10487. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  10488. }
  10489. // recalculate the size of the node on the next time the node is rendered
  10490. parentNode.clearSizeCache();
  10491. // set the pop-out scale for the childnode
  10492. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  10493. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  10494. childNode.clearVelocity();
  10495. // the mass has altered, preservation of energy dictates the velocity to be updated
  10496. parentNode.updateVelocity(massBefore);
  10497. // restart the simulation to reorganise all nodes
  10498. this.moving = true;
  10499. },
  10500. /**
  10501. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  10502. * This is a seperate function to allow for level-wise collapsing of the node tree.
  10503. * It has to be called if a level is collapsed. It is called by _formClusters().
  10504. * @private
  10505. */
  10506. _updateDynamicEdges : function() {
  10507. for (var i = 0; i < this.nodeIndices.length; i++) {
  10508. var node = this.nodes[this.nodeIndices[i]];
  10509. node.dynamicEdgesLength = node.dynamicEdges.length;
  10510. // this corrects for multiple edges pointing at the same other node
  10511. var correction = 0;
  10512. if (node.dynamicEdgesLength > 1) {
  10513. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  10514. var edgeToId = node.dynamicEdges[j].toId;
  10515. var edgeFromId = node.dynamicEdges[j].fromId;
  10516. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  10517. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  10518. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  10519. correction += 1;
  10520. }
  10521. }
  10522. }
  10523. }
  10524. node.dynamicEdgesLength -= correction;
  10525. }
  10526. },
  10527. /**
  10528. * This adds an edge from the childNode to the contained edges of the parent node
  10529. *
  10530. * @param parentNode | Node object
  10531. * @param childNode | Node object
  10532. * @param edge | Edge object
  10533. * @private
  10534. */
  10535. _addToContainedEdges : function(parentNode, childNode, edge) {
  10536. // create an array object if it does not yet exist for this childNode
  10537. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  10538. parentNode.containedEdges[childNode.id] = []
  10539. }
  10540. // add this edge to the list
  10541. parentNode.containedEdges[childNode.id].push(edge);
  10542. // remove the edge from the global edges object
  10543. delete this.edges[edge.id];
  10544. // remove the edge from the parent object
  10545. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  10546. if (parentNode.dynamicEdges[i].id == edge.id) {
  10547. parentNode.dynamicEdges.splice(i,1);
  10548. break;
  10549. }
  10550. }
  10551. },
  10552. /**
  10553. * This function connects an edge that was connected to a child node to the parent node.
  10554. * It keeps track of which nodes it has been connected to with the originalId array.
  10555. *
  10556. * @param {Node} parentNode | Node object
  10557. * @param {Node} childNode | Node object
  10558. * @param {Edge} edge | Edge object
  10559. * @private
  10560. */
  10561. _connectEdgeToCluster : function(parentNode, childNode, edge) {
  10562. // handle circular edges
  10563. if (edge.toId == edge.fromId) {
  10564. this._addToContainedEdges(parentNode, childNode, edge);
  10565. }
  10566. else {
  10567. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  10568. edge.originalToId.push(childNode.id);
  10569. edge.to = parentNode;
  10570. edge.toId = parentNode.id;
  10571. }
  10572. else { // edge connected to other node with the "from" side
  10573. edge.originalFromId.push(childNode.id);
  10574. edge.from = parentNode;
  10575. edge.fromId = parentNode.id;
  10576. }
  10577. this._addToReroutedEdges(parentNode,childNode,edge);
  10578. }
  10579. },
  10580. _containCircularEdgesFromNode : function(parentNode, childNode) {
  10581. // manage all the edges connected to the child and parent nodes
  10582. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  10583. var edge = parentNode.dynamicEdges[i];
  10584. // handle circular edges
  10585. if (edge.toId == edge.fromId) {
  10586. this._addToContainedEdges(parentNode, childNode, edge);
  10587. }
  10588. }
  10589. },
  10590. /**
  10591. * This adds an edge from the childNode to the rerouted edges of the parent node
  10592. *
  10593. * @param parentNode | Node object
  10594. * @param childNode | Node object
  10595. * @param edge | Edge object
  10596. * @private
  10597. */
  10598. _addToReroutedEdges : function(parentNode, childNode, edge) {
  10599. // create an array object if it does not yet exist for this childNode
  10600. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  10601. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  10602. parentNode.reroutedEdges[childNode.id] = [];
  10603. }
  10604. parentNode.reroutedEdges[childNode.id].push(edge);
  10605. // this edge becomes part of the dynamicEdges of the cluster node
  10606. parentNode.dynamicEdges.push(edge);
  10607. },
  10608. /**
  10609. * This function connects an edge that was connected to a cluster node back to the child node.
  10610. *
  10611. * @param parentNode | Node object
  10612. * @param childNode | Node object
  10613. * @private
  10614. */
  10615. _connectEdgeBackToChild : function(parentNode, childNode) {
  10616. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  10617. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  10618. var edge = parentNode.reroutedEdges[childNode.id][i];
  10619. if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
  10620. edge.originalFromId.pop();
  10621. edge.fromId = childNode.id;
  10622. edge.from = childNode;
  10623. }
  10624. else {
  10625. edge.originalToId.pop();
  10626. edge.toId = childNode.id;
  10627. edge.to = childNode;
  10628. }
  10629. // append this edge to the list of edges connecting to the childnode
  10630. childNode.dynamicEdges.push(edge);
  10631. // remove the edge from the parent object
  10632. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  10633. if (parentNode.dynamicEdges[j].id == edge.id) {
  10634. parentNode.dynamicEdges.splice(j,1);
  10635. break;
  10636. }
  10637. }
  10638. }
  10639. // remove the entry from the rerouted edges
  10640. delete parentNode.reroutedEdges[childNode.id];
  10641. }
  10642. },
  10643. /**
  10644. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  10645. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  10646. * parentNode
  10647. *
  10648. * @param parentNode | Node object
  10649. * @private
  10650. */
  10651. _validateEdges : function(parentNode) {
  10652. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  10653. var edge = parentNode.dynamicEdges[i];
  10654. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  10655. parentNode.dynamicEdges.splice(i,1);
  10656. }
  10657. }
  10658. },
  10659. /**
  10660. * This function released the contained edges back into the global domain and puts them back into the
  10661. * dynamic edges of both parent and child.
  10662. *
  10663. * @param {Node} parentNode |
  10664. * @param {Node} childNode |
  10665. * @private
  10666. */
  10667. _releaseContainedEdges : function(parentNode, childNode) {
  10668. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  10669. var edge = parentNode.containedEdges[childNode.id][i];
  10670. // put the edge back in the global edges object
  10671. this.edges[edge.id] = edge;
  10672. // put the edge back in the dynamic edges of the child and parent
  10673. childNode.dynamicEdges.push(edge);
  10674. parentNode.dynamicEdges.push(edge);
  10675. }
  10676. // remove the entry from the contained edges
  10677. delete parentNode.containedEdges[childNode.id];
  10678. },
  10679. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  10680. /**
  10681. * This updates the node labels for all nodes (for debugging purposes)
  10682. */
  10683. updateLabels : function() {
  10684. var nodeId;
  10685. // update node labels
  10686. for (nodeId in this.nodes) {
  10687. if (this.nodes.hasOwnProperty(nodeId)) {
  10688. var node = this.nodes[nodeId];
  10689. if (node.clusterSize > 1) {
  10690. node.label = "[".concat(String(node.clusterSize),"]");
  10691. }
  10692. }
  10693. }
  10694. // update node labels
  10695. for (nodeId in this.nodes) {
  10696. if (this.nodes.hasOwnProperty(nodeId)) {
  10697. node = this.nodes[nodeId];
  10698. if (node.clusterSize == 1) {
  10699. if (node.originalLabel !== undefined) {
  10700. node.label = node.originalLabel;
  10701. }
  10702. else {
  10703. node.label = String(node.id);
  10704. }
  10705. }
  10706. }
  10707. }
  10708. /* Debug Override */
  10709. // for (nodeId in this.nodes) {
  10710. // if (this.nodes.hasOwnProperty(nodeId)) {
  10711. // node = this.nodes[nodeId];
  10712. // node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale));
  10713. // }
  10714. // }
  10715. },
  10716. /**
  10717. * This function determines if the cluster we want to decluster is in the active area
  10718. * this means around the zoom center
  10719. *
  10720. * @param {Node} node
  10721. * @returns {boolean}
  10722. * @private
  10723. */
  10724. _nodeInActiveArea : function(node) {
  10725. return (
  10726. Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  10727. &&
  10728. Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  10729. )
  10730. },
  10731. /**
  10732. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  10733. * It puts large clusters away from the center and randomizes the order.
  10734. *
  10735. */
  10736. repositionNodes : function() {
  10737. for (var i = 0; i < this.nodeIndices.length; i++) {
  10738. var node = this.nodes[this.nodeIndices[i]];
  10739. if (!node.isFixed()) {
  10740. var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize);
  10741. var angle = 2 * Math.PI * Math.random();
  10742. node.x = radius * Math.cos(angle);
  10743. node.y = radius * Math.sin(angle);
  10744. }
  10745. }
  10746. },
  10747. /**
  10748. * We determine how many connections denote an important hub.
  10749. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  10750. *
  10751. * @private
  10752. */
  10753. _getHubSize : function() {
  10754. var average = 0;
  10755. var averageSquared = 0;
  10756. var hubCounter = 0;
  10757. var largestHub = 0;
  10758. for (var i = 0; i < this.nodeIndices.length; i++) {
  10759. var node = this.nodes[this.nodeIndices[i]];
  10760. if (node.dynamicEdgesLength > largestHub) {
  10761. largestHub = node.dynamicEdgesLength;
  10762. }
  10763. average += node.dynamicEdgesLength;
  10764. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  10765. hubCounter += 1;
  10766. }
  10767. average = average / hubCounter;
  10768. averageSquared = averageSquared / hubCounter;
  10769. var variance = averageSquared - Math.pow(average,2);
  10770. var standardDeviation = Math.sqrt(variance);
  10771. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  10772. // always have at least one to cluster
  10773. if (this.hubThreshold > largestHub) {
  10774. this.hubThreshold = largestHub;
  10775. }
  10776. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  10777. // console.log("hubThreshold:",this.hubThreshold);
  10778. },
  10779. /**
  10780. * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  10781. * with this amount we can cluster specifically on these chains.
  10782. *
  10783. * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
  10784. * @private
  10785. */
  10786. _reduceAmountOfChains : function(fraction) {
  10787. this.hubThreshold = 2;
  10788. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  10789. for (var nodeId in this.nodes) {
  10790. if (this.nodes.hasOwnProperty(nodeId)) {
  10791. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  10792. if (reduceAmount > 0) {
  10793. this._formClusterFromHub(this.nodes[nodeId],true,true,1);
  10794. reduceAmount -= 1;
  10795. }
  10796. }
  10797. }
  10798. }
  10799. },
  10800. /**
  10801. * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
  10802. * with this amount we can cluster specifically on these chains.
  10803. *
  10804. * @private
  10805. */
  10806. _getChainFraction : function() {
  10807. var chains = 0;
  10808. var total = 0;
  10809. for (var nodeId in this.nodes) {
  10810. if (this.nodes.hasOwnProperty(nodeId)) {
  10811. if (this.nodes[nodeId].dynamicEdgesLength == 2 && this.nodes[nodeId].dynamicEdges.length >= 2) {
  10812. chains += 1;
  10813. }
  10814. total += 1;
  10815. }
  10816. }
  10817. return chains/total;
  10818. }
  10819. };
  10820. var SelectionMixin = {
  10821. /**
  10822. * This function can be called from the _doInAllSectors function
  10823. *
  10824. * @param object
  10825. * @param overlappingNodes
  10826. * @private
  10827. */
  10828. _getNodesOverlappingWith : function(object, overlappingNodes) {
  10829. var nodes = this.nodes;
  10830. for (var nodeId in nodes) {
  10831. if (nodes.hasOwnProperty(nodeId)) {
  10832. if (nodes[nodeId].isOverlappingWith(object)) {
  10833. overlappingNodes.push(nodeId);
  10834. }
  10835. }
  10836. }
  10837. },
  10838. /**
  10839. * retrieve all nodes overlapping with given object
  10840. * @param {Object} object An object with parameters left, top, right, bottom
  10841. * @return {Number[]} An array with id's of the overlapping nodes
  10842. * @private
  10843. */
  10844. _getAllNodesOverlappingWith : function (object) {
  10845. var overlappingNodes = [];
  10846. this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
  10847. return overlappingNodes;
  10848. },
  10849. /**
  10850. * retrieve all nodes in the navigation controls overlapping with given object
  10851. * @param {Object} object An object with parameters left, top, right, bottom
  10852. * @return {Number[]} An array with id's of the overlapping nodes
  10853. * @private
  10854. */
  10855. _getAllNavigationNodesOverlappingWith : function (object) {
  10856. var overlappingNodes = [];
  10857. this._doInNavigationSector("_getNodesOverlappingWith",object,overlappingNodes);
  10858. return overlappingNodes;
  10859. },
  10860. /**
  10861. * Return a position object in canvasspace from a single point in screenspace
  10862. *
  10863. * @param pointer
  10864. * @returns {{left: number, top: number, right: number, bottom: number}}
  10865. * @private
  10866. */
  10867. _pointerToPositionObject : function(pointer) {
  10868. var x = this._canvasToX(pointer.x);
  10869. var y = this._canvasToY(pointer.y);
  10870. return {left: x,
  10871. top: y,
  10872. right: x,
  10873. bottom: y};
  10874. },
  10875. /**
  10876. * Return a position object in canvasspace from a single point in screenspace
  10877. *
  10878. * @param pointer
  10879. * @returns {{left: number, top: number, right: number, bottom: number}}
  10880. * @private
  10881. */
  10882. _pointerToScreenPositionObject : function(pointer) {
  10883. var x = pointer.x;
  10884. var y = pointer.y;
  10885. return {left: x,
  10886. top: y,
  10887. right: x,
  10888. bottom: y};
  10889. },
  10890. /**
  10891. * Get the top navigation controls node at the a specific point (like a click)
  10892. *
  10893. * @param {{x: Number, y: Number}} pointer
  10894. * @return {Node | null} node
  10895. * @private
  10896. */
  10897. _getNavigationNodeAt : function (pointer) {
  10898. var screenPositionObject = this._pointerToScreenPositionObject(pointer);
  10899. var overlappingNodes = this._getAllNavigationNodesOverlappingWith(screenPositionObject);
  10900. if (overlappingNodes.length > 0) {
  10901. return this.sectors["navigation"]["nodes"][overlappingNodes[overlappingNodes.length - 1]];
  10902. }
  10903. else {
  10904. return null;
  10905. }
  10906. },
  10907. /**
  10908. * Get the top node at the a specific point (like a click)
  10909. *
  10910. * @param {{x: Number, y: Number}} pointer
  10911. * @return {Node | null} node
  10912. * @private
  10913. */
  10914. _getNodeAt : function (pointer) {
  10915. // we first check if this is an navigation controls element
  10916. var positionObject = this._pointerToPositionObject(pointer);
  10917. overlappingNodes = this._getAllNodesOverlappingWith(positionObject);
  10918. // if there are overlapping nodes, select the last one, this is the
  10919. // one which is drawn on top of the others
  10920. if (overlappingNodes.length > 0) {
  10921. return this.nodes[overlappingNodes[overlappingNodes.length - 1]];
  10922. }
  10923. else {
  10924. return null;
  10925. }
  10926. },
  10927. /**
  10928. * Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call
  10929. * _getNodeAt and _getEdgesAt, then priortize the selection to user preferences.
  10930. *
  10931. * @param pointer
  10932. * @returns {null}
  10933. * @private
  10934. */
  10935. _getEdgeAt : function(pointer) {
  10936. return null;
  10937. },
  10938. /**
  10939. * Add object to the selection array. The this.selection id array may not be needed.
  10940. *
  10941. * @param obj
  10942. * @private
  10943. */
  10944. _addToSelection : function(obj) {
  10945. this.selection.push(obj.id);
  10946. this.selectionObj[obj.id] = obj;
  10947. },
  10948. /**
  10949. * Remove a single option from selection.
  10950. *
  10951. * @param obj
  10952. * @private
  10953. */
  10954. _removeFromSelection : function(obj) {
  10955. for (var i = 0; i < this.selection.length; i++) {
  10956. if (obj.id == this.selection[i]) {
  10957. this.selection.splice(i,1);
  10958. break;
  10959. }
  10960. }
  10961. delete this.selectionObj[obj.id];
  10962. },
  10963. /**
  10964. * Unselect all. The selectionObj is useful for this.
  10965. *
  10966. * @param {Boolean} [doNotTrigger] | ignore trigger
  10967. * @private
  10968. */
  10969. _unselectAll : function(doNotTrigger) {
  10970. if (doNotTrigger === undefined) {
  10971. doNotTrigger = false;
  10972. }
  10973. this.selection = [];
  10974. for (var objId in this.selectionObj) {
  10975. if (this.selectionObj.hasOwnProperty(objId)) {
  10976. this.selectionObj[objId].unselect();
  10977. }
  10978. }
  10979. this.selectionObj = {};
  10980. if (doNotTrigger == false) {
  10981. this._trigger('select');
  10982. }
  10983. },
  10984. /**
  10985. * Check if anything is selected
  10986. *
  10987. * @returns {boolean}
  10988. * @private
  10989. */
  10990. _selectionIsEmpty : function() {
  10991. if (this.selection.length == 0) {
  10992. return true;
  10993. }
  10994. else {
  10995. return false;
  10996. }
  10997. },
  10998. /**
  10999. * This is called when someone clicks on a node. either select or deselect it.
  11000. * If there is an existing selection and we don't want to append to it, clear the existing selection
  11001. *
  11002. * @param {Node} node
  11003. * @param {Boolean} append
  11004. * @param {Boolean} [doNotTrigger] | ignore trigger
  11005. * @private
  11006. */
  11007. _selectNode : function(node, append, doNotTrigger) {
  11008. if (doNotTrigger === undefined) {
  11009. doNotTrigger = false;
  11010. }
  11011. if (this._selectionIsEmpty() == false && append == false) {
  11012. this._unselectAll(true);
  11013. }
  11014. if (node.selected == false) {
  11015. node.select();
  11016. this._addToSelection(node);
  11017. }
  11018. else {
  11019. node.unselect();
  11020. this._removeFromSelection(node);
  11021. }
  11022. if (doNotTrigger == false) {
  11023. this._trigger('select');
  11024. }
  11025. },
  11026. /**
  11027. * handles the selection part of the touch, only for navigation controls elements;
  11028. * Touch is triggered before tap, also before hold. Hold triggers after a while.
  11029. * This is the most responsive solution
  11030. *
  11031. * @param {Object} pointer
  11032. * @private
  11033. */
  11034. _handleTouch : function(pointer) {
  11035. if (this.constants.navigation.enabled == true) {
  11036. var node = this._getNavigationNodeAt(pointer);
  11037. if (node != null) {
  11038. if (this[node.triggerFunction] !== undefined) {
  11039. this[node.triggerFunction]();
  11040. }
  11041. }
  11042. }
  11043. },
  11044. /**
  11045. * handles the selection part of the tap;
  11046. *
  11047. * @param {Object} pointer
  11048. * @private
  11049. */
  11050. _handleTap : function(pointer) {
  11051. var node = this._getNodeAt(pointer);
  11052. if (node != null) {
  11053. this._selectNode(node,false);
  11054. }
  11055. else {
  11056. this._unselectAll();
  11057. }
  11058. this._redraw();
  11059. },
  11060. /**
  11061. * handles the selection part of the double tap and opens a cluster if needed
  11062. *
  11063. * @param {Object} pointer
  11064. * @private
  11065. */
  11066. _handleDoubleTap : function(pointer) {
  11067. var node = this._getNodeAt(pointer);
  11068. if (node != null && node !== undefined) {
  11069. // we reset the areaCenter here so the opening of the node will occur
  11070. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  11071. "y" : this._canvasToY(pointer.y)};
  11072. this.openCluster(node);
  11073. }
  11074. },
  11075. /**
  11076. * Handle the onHold selection part
  11077. *
  11078. * @param pointer
  11079. * @private
  11080. */
  11081. _handleOnHold : function(pointer) {
  11082. var node = this._getNodeAt(pointer);
  11083. if (node != null) {
  11084. this._selectNode(node,true);
  11085. }
  11086. this._redraw();
  11087. },
  11088. /**
  11089. * handle the onRelease event. These functions are here for the navigation controls module.
  11090. *
  11091. * @private
  11092. */
  11093. _handleOnRelease : function() {
  11094. this.xIncrement = 0;
  11095. this.yIncrement = 0;
  11096. this.zoomIncrement = 0;
  11097. this._unHighlightAll();
  11098. },
  11099. /**
  11100. *
  11101. * retrieve the currently selected nodes
  11102. * @return {Number[] | String[]} selection An array with the ids of the
  11103. * selected nodes.
  11104. */
  11105. getSelection : function() {
  11106. return this.selection.concat([]);
  11107. },
  11108. /**
  11109. *
  11110. * retrieve the currently selected nodes as objects
  11111. * @return {Objects} selection An array with the ids of the
  11112. * selected nodes.
  11113. */
  11114. getSelectionObjects : function() {
  11115. return this.selectionObj;
  11116. },
  11117. /**
  11118. * // TODO: rework this function, it is from the old system
  11119. *
  11120. * select zero or more nodes
  11121. * @param {Number[] | String[]} selection An array with the ids of the
  11122. * selected nodes.
  11123. */
  11124. setSelection : function(selection) {
  11125. var i, iMax, id;
  11126. if (!selection || (selection.length == undefined))
  11127. throw 'Selection must be an array with ids';
  11128. // first unselect any selected node
  11129. this._unselectAll(true);
  11130. for (i = 0, iMax = selection.length; i < iMax; i++) {
  11131. id = selection[i];
  11132. var node = this.nodes[id];
  11133. if (!node) {
  11134. throw new RangeError('Node with id "' + id + '" not found');
  11135. }
  11136. this._selectNode(node,true,true);
  11137. }
  11138. this.redraw();
  11139. },
  11140. /**
  11141. * TODO: rework this function, it is from the old system
  11142. *
  11143. * Validate the selection: remove ids of nodes which no longer exist
  11144. * @private
  11145. */
  11146. _updateSelection : function () {
  11147. var i = 0;
  11148. while (i < this.selection.length) {
  11149. var nodeId = this.selection[i];
  11150. if (!this.nodes.hasOwnProperty(nodeId)) {
  11151. this.selection.splice(i, 1);
  11152. delete this.selectionObj[nodeId];
  11153. }
  11154. else {
  11155. i++;
  11156. }
  11157. }
  11158. }
  11159. /**
  11160. * Unselect selected nodes. If no selection array is provided, all nodes
  11161. * are unselected
  11162. * @param {Object[]} selection Array with selection objects, each selection
  11163. * object has a parameter row. Optional
  11164. * @param {Boolean} triggerSelect If true (default), the select event
  11165. * is triggered when nodes are unselected
  11166. * @return {Boolean} changed True if the selection is changed
  11167. * @private
  11168. */
  11169. /* _unselectNodes : function(selection, triggerSelect) {
  11170. var changed = false;
  11171. var i, iMax, id;
  11172. if (selection) {
  11173. // remove provided selections
  11174. for (i = 0, iMax = selection.length; i < iMax; i++) {
  11175. id = selection[i];
  11176. if (this.nodes.hasOwnProperty(id)) {
  11177. this.nodes[id].unselect();
  11178. }
  11179. var j = 0;
  11180. while (j < this.selection.length) {
  11181. if (this.selection[j] == id) {
  11182. this.selection.splice(j, 1);
  11183. changed = true;
  11184. }
  11185. else {
  11186. j++;
  11187. }
  11188. }
  11189. }
  11190. }
  11191. else if (this.selection && this.selection.length) {
  11192. // remove all selections
  11193. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  11194. id = this.selection[i];
  11195. if (this.nodes.hasOwnProperty(id)) {
  11196. this.nodes[id].unselect();
  11197. }
  11198. changed = true;
  11199. }
  11200. this.selection = [];
  11201. }
  11202. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  11203. // fire the select event
  11204. this._trigger('select');
  11205. }
  11206. return changed;
  11207. },
  11208. */
  11209. /**
  11210. * select all nodes on given location x, y
  11211. * @param {Array} selection an array with node ids
  11212. * @param {boolean} append If true, the new selection will be appended to the
  11213. * current selection (except for duplicate entries)
  11214. * @return {Boolean} changed True if the selection is changed
  11215. * @private
  11216. */
  11217. /* _selectNodes : function(selection, append) {
  11218. var changed = false;
  11219. var i, iMax;
  11220. // TODO: the selectNodes method is a little messy, rework this
  11221. // check if the current selection equals the desired selection
  11222. var selectionAlreadyThere = true;
  11223. if (selection.length != this.selection.length) {
  11224. selectionAlreadyThere = false;
  11225. }
  11226. else {
  11227. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  11228. if (selection[i] != this.selection[i]) {
  11229. selectionAlreadyThere = false;
  11230. break;
  11231. }
  11232. }
  11233. }
  11234. if (selectionAlreadyThere) {
  11235. return changed;
  11236. }
  11237. if (append == undefined || append == false) {
  11238. // first deselect any selected node
  11239. var triggerSelect = false;
  11240. changed = this._unselectNodes(undefined, triggerSelect);
  11241. }
  11242. for (i = 0, iMax = selection.length; i < iMax; i++) {
  11243. // add each of the new selections, but only when they are not duplicate
  11244. var id = selection[i];
  11245. var isDuplicate = (this.selection.indexOf(id) != -1);
  11246. if (!isDuplicate) {
  11247. this.nodes[id].select();
  11248. this.selection.push(id);
  11249. changed = true;
  11250. }
  11251. }
  11252. if (changed) {
  11253. // fire the select event
  11254. this._trigger('select');
  11255. }
  11256. return changed;
  11257. },
  11258. */
  11259. };
  11260. /**
  11261. * Created by Alex on 1/22/14.
  11262. */
  11263. var NavigationMixin = {
  11264. /**
  11265. * This function moves the navigation controls if the canvas size has been changed. If the arugments
  11266. * verticaAlignTop and horizontalAlignLeft are false, the correction will be made
  11267. *
  11268. * @private
  11269. */
  11270. _relocateNavigation : function() {
  11271. if (this.sectors !== undefined) {
  11272. var xOffset = this.navigationClientWidth - this.frame.canvas.clientWidth;
  11273. var yOffset = this.navigationClientHeight - this.frame.canvas.clientHeight;
  11274. this.navigationClientWidth = this.frame.canvas.clientWidth;
  11275. this.navigationClientHeight = this.frame.canvas.clientHeight;
  11276. var node = null;
  11277. for (var nodeId in this.sectors["navigation"]["nodes"]) {
  11278. if (this.sectors["navigation"]["nodes"].hasOwnProperty(nodeId)) {
  11279. node = this.sectors["navigation"]["nodes"][nodeId];
  11280. if (!node.horizontalAlignLeft) {
  11281. node.x -= xOffset;
  11282. }
  11283. if (!node.verticalAlignTop) {
  11284. node.y -= yOffset;
  11285. }
  11286. }
  11287. }
  11288. }
  11289. },
  11290. /**
  11291. * Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
  11292. * they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
  11293. * on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
  11294. * This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
  11295. *
  11296. * @private
  11297. */
  11298. _loadNavigationElements : function() {
  11299. var DIR = this.constants.navigation.iconPath;
  11300. this.navigationClientWidth = this.frame.canvas.clientWidth;
  11301. this.navigationClientHeight = this.frame.canvas.clientHeight;
  11302. if (this.navigationClientWidth === undefined) {
  11303. this.navigationClientWidth = 0;
  11304. this.navigationClientHeight = 0;
  11305. }
  11306. var offset = 15;
  11307. var intermediateOffset = 7;
  11308. var navigationNodes = [
  11309. {id: 'navigation_up', shape: 'image', image: DIR + 'uparrow.png', triggerFunction: "_moveUp",
  11310. verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 45 - offset - intermediateOffset},
  11311. {id: 'navigation_down', shape: 'image', image: DIR + 'downarrow.png', triggerFunction: "_moveDown",
  11312. verticalAlignTop: false, x: 45 + offset + intermediateOffset, y: this.navigationClientHeight - 15 - offset},
  11313. {id: 'navigation_left', shape: 'image', image: DIR + 'leftarrow.png', triggerFunction: "_moveLeft",
  11314. verticalAlignTop: false, x: 15 + offset, y: this.navigationClientHeight - 15 - offset},
  11315. {id: 'navigation_right', shape: 'image', image: DIR + 'rightarrow.png',triggerFunction: "_moveRight",
  11316. verticalAlignTop: false, x: 75 + offset + 2 * intermediateOffset, y: this.navigationClientHeight - 15 - offset},
  11317. {id: 'navigation_plus', shape: 'image', image: DIR + 'plus.png', triggerFunction: "_zoomIn",
  11318. verticalAlignTop: false, horizontalAlignLeft: false,
  11319. x: this.navigationClientWidth - 45 - offset - intermediateOffset, y: this.navigationClientHeight - 15 - offset},
  11320. {id: 'navigation_min', shape: 'image', image: DIR + 'minus.png', triggerFunction: "_zoomOut",
  11321. verticalAlignTop: false, horizontalAlignLeft: false,
  11322. x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 15 - offset},
  11323. {id: 'navigation_zoomExtends', shape: 'image', image: DIR + 'zoomExtends.png', triggerFunction: "zoomToFit",
  11324. verticalAlignTop: false, horizontalAlignLeft: false,
  11325. x: this.navigationClientWidth - 15 - offset, y: this.navigationClientHeight - 45 - offset - intermediateOffset}
  11326. ];
  11327. var nodeObj = null;
  11328. for (var i = 0; i < navigationNodes.length; i++) {
  11329. nodeObj = this.sectors["navigation"]['nodes'];
  11330. nodeObj[navigationNodes[i]['id']] = new Node(navigationNodes[i], this.images, this.groups, this.constants);
  11331. }
  11332. },
  11333. /**
  11334. * By setting the clustersize to be larger than 1, we use the clustering drawing method
  11335. * to illustrate the buttons are presed. We call this highlighting.
  11336. *
  11337. * @param {String} elementId
  11338. * @private
  11339. */
  11340. _highlightNavigationElement : function(elementId) {
  11341. if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) {
  11342. this.sectors["navigation"]["nodes"][elementId].clusterSize = 2;
  11343. }
  11344. },
  11345. /**
  11346. * Reverting back to a normal button
  11347. *
  11348. * @param {String} elementId
  11349. * @private
  11350. */
  11351. _unHighlightNavigationElement : function(elementId) {
  11352. if (this.sectors["navigation"]["nodes"].hasOwnProperty(elementId)) {
  11353. this.sectors["navigation"]["nodes"][elementId].clusterSize = 1;
  11354. }
  11355. },
  11356. /**
  11357. * un-highlight (for lack of a better term) all navigation controls elements
  11358. * @private
  11359. */
  11360. _unHighlightAll : function() {
  11361. for (var nodeId in this.sectors['navigation']['nodes']) {
  11362. if (this.sectors['navigation']['nodes'].hasOwnProperty(nodeId)) {
  11363. this._unHighlightNavigationElement(nodeId);
  11364. }
  11365. }
  11366. },
  11367. _preventDefault : function(event) {
  11368. if (event !== undefined) {
  11369. if (event.preventDefault) {
  11370. event.preventDefault();
  11371. } else {
  11372. event.returnValue = false;
  11373. }
  11374. }
  11375. },
  11376. /**
  11377. * move the screen up
  11378. * By using the increments, instead of adding a fixed number to the translation, we keep fluent and
  11379. * instant movement. The onKeypress event triggers immediately, then pauses, then triggers frequently
  11380. * To avoid this behaviour, we do the translation in the start loop.
  11381. *
  11382. * @private
  11383. */
  11384. _moveUp : function(event) {
  11385. this._highlightNavigationElement("navigation_up");
  11386. this.yIncrement = this.constants.keyboard.speed.y;
  11387. this.start(); // if there is no node movement, the calculation wont be done
  11388. this._preventDefault(event);
  11389. },
  11390. /**
  11391. * move the screen down
  11392. * @private
  11393. */
  11394. _moveDown : function(event) {
  11395. this._highlightNavigationElement("navigation_down");
  11396. this.yIncrement = -this.constants.keyboard.speed.y;
  11397. this.start(); // if there is no node movement, the calculation wont be done
  11398. this._preventDefault(event);
  11399. },
  11400. /**
  11401. * move the screen left
  11402. * @private
  11403. */
  11404. _moveLeft : function(event) {
  11405. this._highlightNavigationElement("navigation_left");
  11406. this.xIncrement = this.constants.keyboard.speed.x;
  11407. this.start(); // if there is no node movement, the calculation wont be done
  11408. this._preventDefault(event);
  11409. },
  11410. /**
  11411. * move the screen right
  11412. * @private
  11413. */
  11414. _moveRight : function(event) {
  11415. this._highlightNavigationElement("navigation_right");
  11416. this.xIncrement = -this.constants.keyboard.speed.y;
  11417. this.start(); // if there is no node movement, the calculation wont be done
  11418. this._preventDefault(event);
  11419. },
  11420. /**
  11421. * Zoom in, using the same method as the movement.
  11422. * @private
  11423. */
  11424. _zoomIn : function(event) {
  11425. this._highlightNavigationElement("navigation_plus");
  11426. this.zoomIncrement = this.constants.keyboard.speed.zoom;
  11427. this.start(); // if there is no node movement, the calculation wont be done
  11428. this._preventDefault(event);
  11429. },
  11430. /**
  11431. * Zoom out
  11432. * @private
  11433. */
  11434. _zoomOut : function() {
  11435. this._highlightNavigationElement("navigation_min");
  11436. this.zoomIncrement = -this.constants.keyboard.speed.zoom;
  11437. this.start(); // if there is no node movement, the calculation wont be done
  11438. this._preventDefault(event);
  11439. },
  11440. /**
  11441. * Stop zooming and unhighlight the zoom controls
  11442. * @private
  11443. */
  11444. _stopZoom : function() {
  11445. this._unHighlightNavigationElement("navigation_plus");
  11446. this._unHighlightNavigationElement("navigation_min");
  11447. this.zoomIncrement = 0;
  11448. },
  11449. /**
  11450. * Stop moving in the Y direction and unHighlight the up and down
  11451. * @private
  11452. */
  11453. _yStopMoving : function() {
  11454. this._unHighlightNavigationElement("navigation_up");
  11455. this._unHighlightNavigationElement("navigation_down");
  11456. this.yIncrement = 0;
  11457. },
  11458. /**
  11459. * Stop moving in the X direction and unHighlight left and right.
  11460. * @private
  11461. */
  11462. _xStopMoving : function() {
  11463. this._unHighlightNavigationElement("navigation_left");
  11464. this._unHighlightNavigationElement("navigation_right");
  11465. this.xIncrement = 0;
  11466. }
  11467. };
  11468. /**
  11469. * @constructor Graph
  11470. * Create a graph visualization, displaying nodes and edges.
  11471. *
  11472. * @param {Element} container The DOM element in which the Graph will
  11473. * be created. Normally a div element.
  11474. * @param {Object} data An object containing parameters
  11475. * {Array} nodes
  11476. * {Array} edges
  11477. * @param {Object} options Options
  11478. */
  11479. function Graph (container, data, options) {
  11480. // create variables and set default values
  11481. this.containerElement = container;
  11482. this.width = '100%';
  11483. this.height = '100%';
  11484. // to give everything a nice fluidity, we seperate the rendering and calculating of the forces
  11485. this.renderRefreshRate = 60; // hz (fps)
  11486. this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
  11487. this.stabilize = true; // stabilize before displaying the graph
  11488. this.selectable = true;
  11489. this.forceFactor = 50000;
  11490. // set constant values
  11491. this.constants = {
  11492. nodes: {
  11493. radiusMin: 5,
  11494. radiusMax: 20,
  11495. radius: 5,
  11496. distance: 100, // px
  11497. shape: 'ellipse',
  11498. image: undefined,
  11499. widthMin: 16, // px
  11500. widthMax: 64, // px
  11501. fontColor: 'black',
  11502. fontSize: 14, // px
  11503. //fontFace: verdana,
  11504. fontFace: 'arial',
  11505. color: {
  11506. border: '#2B7CE9',
  11507. background: '#97C2FC',
  11508. highlight: {
  11509. border: '#2B7CE9',
  11510. background: '#D2E5FF'
  11511. }
  11512. },
  11513. borderColor: '#2B7CE9',
  11514. backgroundColor: '#97C2FC',
  11515. highlightColor: '#D2E5FF',
  11516. group: undefined
  11517. },
  11518. edges: {
  11519. widthMin: 1,
  11520. widthMax: 15,
  11521. width: 1,
  11522. style: 'line',
  11523. color: '#343434',
  11524. fontColor: '#343434',
  11525. fontSize: 14, // px
  11526. fontFace: 'arial',
  11527. //distance: 100, //px
  11528. length: 100, // px
  11529. dash: {
  11530. length: 10,
  11531. gap: 5,
  11532. altLength: undefined
  11533. }
  11534. },
  11535. clustering: { // Per Node in Cluster = PNiC
  11536. enabled: false, // (Boolean) | global on/off switch for clustering.
  11537. initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
  11538. 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
  11539. 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
  11540. chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
  11541. clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
  11542. sectorThreshold: 50, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
  11543. 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.
  11544. fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
  11545. forceAmplification: 0.6, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
  11546. distanceAmplification: 0.2, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
  11547. edgeGrowth: 11, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
  11548. nodeScaling: {width: 10, // (px PNiC) | growth of the width per node in cluster.
  11549. height: 10, // (px PNiC) | growth of the height per node in cluster.
  11550. radius: 10}, // (px PNiC) | growth of the radius per node in cluster.
  11551. activeAreaBoxSize: 100, // (px) | box area around the curser where clusters are popped open.
  11552. massTransferCoefficient: 1 // (multiplier) | parent.mass += massTransferCoefficient * child.mass
  11553. },
  11554. navigation: {
  11555. enabled: false,
  11556. iconPath: this._getIconURL()
  11557. },
  11558. keyboard: {
  11559. enabled: false,
  11560. speed: {x: 10, y: 10, zoom: 0.02}
  11561. },
  11562. minVelocity: 2, // px/s
  11563. maxIterations: 1000 // maximum number of iteration to stabilize
  11564. };
  11565. // Node variables
  11566. this.groups = new Groups(); // object with groups
  11567. this.images = new Images(); // object with images
  11568. this.images.setOnloadCallback(function () {
  11569. graph._redraw();
  11570. });
  11571. // navigation variables
  11572. this.xIncrement = 0;
  11573. this.yIncrement = 0;
  11574. this.zoomIncrement = 0;
  11575. // create a frame and canvas
  11576. this._create();
  11577. // load the sector system. (mandatory, fully integrated with Graph)
  11578. this._loadSectorSystem();
  11579. // apply options
  11580. this.setOptions(options);
  11581. // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
  11582. this._loadClusterSystem();
  11583. // load the selection system. (mandatory, required by Graph)
  11584. this._loadSelectionSystem();
  11585. // other vars
  11586. var graph = this;
  11587. this.freezeSimulation = false;// freeze the simulation
  11588. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  11589. this.nodes = {}; // object with Node objects
  11590. this.edges = {}; // object with Edge objects
  11591. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
  11592. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
  11593. this.areaCenter = {}; // object with x and y elements used for determining the center of the zoom action
  11594. this.scale = 1; // defining the global scale variable in the constructor
  11595. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  11596. this.lastPointerPosition = {"x": 0,"y": 0}; // this is used for keyboard navigation
  11597. // TODO: create a counter to keep track on the number of nodes having values
  11598. // TODO: create a counter to keep track on the number of nodes currently moving
  11599. // TODO: create a counter to keep track on the number of edges having values
  11600. this.nodesData = null; // A DataSet or DataView
  11601. this.edgesData = null; // A DataSet or DataView
  11602. // create event listeners used to subscribe on the DataSets of the nodes and edges
  11603. var me = this;
  11604. this.nodesListeners = {
  11605. 'add': function (event, params) {
  11606. me._addNodes(params.items);
  11607. me.start();
  11608. },
  11609. 'update': function (event, params) {
  11610. me._updateNodes(params.items);
  11611. me.start();
  11612. },
  11613. 'remove': function (event, params) {
  11614. me._removeNodes(params.items);
  11615. me.start();
  11616. }
  11617. };
  11618. this.edgesListeners = {
  11619. 'add': function (event, params) {
  11620. me._addEdges(params.items);
  11621. me.start();
  11622. },
  11623. 'update': function (event, params) {
  11624. me._updateEdges(params.items);
  11625. me.start();
  11626. },
  11627. 'remove': function (event, params) {
  11628. me._removeEdges(params.items);
  11629. me.start();
  11630. }
  11631. };
  11632. // properties of the data
  11633. this.moving = false; // True if any of the nodes have an undefined position
  11634. this.timer = undefined;
  11635. // load data (the disable start variable will be the same as the enabled clustering)
  11636. this.setData(data,this.constants.clustering.enabled);
  11637. // zoom so all data will fit on the screen
  11638. this.zoomToFit(true);
  11639. // if clustering is disabled, the simulation will have started in the setData function
  11640. if (this.constants.clustering.enabled) {
  11641. this.startWithClustering();
  11642. }
  11643. }
  11644. /**
  11645. * get the URL where the navigation icons are located
  11646. *
  11647. * @returns {string}
  11648. * @private
  11649. */
  11650. Graph.prototype._getIconURL = function() {
  11651. var scripts = document.getElementsByTagName( 'script' );
  11652. var scriptNamePosition, srcPosition, imagePath;
  11653. for (var i = 0; i < scripts.length; i++) {
  11654. srcPosition = scripts[i].outerHTML.search("src");
  11655. if (srcPosition != -1) {
  11656. scriptNamePosition = util.getLowestPositiveNumber(scripts[i].outerHTML.search("vis.js"),
  11657. scripts[i].outerHTML.search("vis.min.js"));
  11658. if (scriptNamePosition != -1) {
  11659. imagePath = scripts[i].outerHTML.substring(srcPosition+5,scriptNamePosition).concat("img/");
  11660. return imagePath;
  11661. }
  11662. }
  11663. }
  11664. return null;
  11665. };
  11666. /**
  11667. * Find the center position of the graph
  11668. * @private
  11669. */
  11670. Graph.prototype._getRange = function() {
  11671. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  11672. for (var i = 0; i < this.nodeIndices.length; i++) {
  11673. node = this.nodes[this.nodeIndices[i]];
  11674. if (minX > (node.x - node.width)) {minX = node.x - node.width;}
  11675. if (maxX < (node.x + node.width)) {maxX = node.x + node.width;}
  11676. if (minY > (node.y - node.height)) {minY = node.y - node.height;}
  11677. if (maxY < (node.y + node.height)) {maxY = node.y + node.height;}
  11678. }
  11679. return {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  11680. };
  11681. /**
  11682. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  11683. * @returns {{x: number, y: number}}
  11684. * @private
  11685. */
  11686. Graph.prototype._findCenter = function(range) {
  11687. var center = {x: (0.5 * (range.maxX + range.minX)),
  11688. y: (0.5 * (range.maxY + range.minY))};
  11689. return center;
  11690. };
  11691. /**
  11692. * center the graph
  11693. *
  11694. * @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY};
  11695. */
  11696. Graph.prototype._centerGraph = function(range) {
  11697. var center = this._findCenter(range);
  11698. center.x *= this.scale;
  11699. center.y *= this.scale;
  11700. center.x -= 0.5 * this.frame.canvas.clientWidth;
  11701. center.y -= 0.5 * this.frame.canvas.clientHeight;
  11702. this._setTranslation(-center.x,-center.y); // set at 0,0
  11703. };
  11704. /**
  11705. * This function zooms out to fit all data on screen based on amount of nodes
  11706. *
  11707. * @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false;
  11708. */
  11709. Graph.prototype.zoomToFit = function(initialZoom) {
  11710. if (initialZoom === undefined) {
  11711. initialZoom = false;
  11712. }
  11713. var numberOfNodes = this.nodeIndices.length;
  11714. var range = this._getRange();
  11715. if (initialZoom == true) {
  11716. if (this.constants.clustering.enabled == true &&
  11717. numberOfNodes >= this.constants.clustering.initialMaxNodes) {
  11718. var zoomLevel = 38.8467 / (numberOfNodes - 14.50184) + 0.0116; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  11719. }
  11720. else {
  11721. var zoomLevel = 42.54117319 / (numberOfNodes + 39.31966387) + 0.1944405; // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  11722. }
  11723. }
  11724. else {
  11725. var xDistance = (Math.abs(range.minX) + Math.abs(range.maxX)) * 1.1;
  11726. var yDistance = (Math.abs(range.minY) + Math.abs(range.maxY)) * 1.1;
  11727. var xZoomLevel = this.frame.canvas.clientWidth / xDistance;
  11728. var yZoomLevel = this.frame.canvas.clientHeight / yDistance;
  11729. zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel;
  11730. }
  11731. if (zoomLevel > 1.0) {
  11732. zoomLevel = 1.0;
  11733. }
  11734. this.pinch.mousewheelScale = zoomLevel;
  11735. this._setScale(zoomLevel);
  11736. this._centerGraph(range);
  11737. this.start();
  11738. };
  11739. /**
  11740. * Update the this.nodeIndices with the most recent node index list
  11741. * @private
  11742. */
  11743. Graph.prototype._updateNodeIndexList = function() {
  11744. this._clearNodeIndexList();
  11745. for (var idx in this.nodes) {
  11746. if (this.nodes.hasOwnProperty(idx)) {
  11747. this.nodeIndices.push(idx);
  11748. }
  11749. }
  11750. };
  11751. /**
  11752. * Set nodes and edges, and optionally options as well.
  11753. *
  11754. * @param {Object} data Object containing parameters:
  11755. * {Array | DataSet | DataView} [nodes] Array with nodes
  11756. * {Array | DataSet | DataView} [edges] Array with edges
  11757. * {String} [dot] String containing data in DOT format
  11758. * {Options} [options] Object with options
  11759. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  11760. */
  11761. Graph.prototype.setData = function(data, disableStart) {
  11762. if (disableStart === undefined) {
  11763. disableStart = false;
  11764. }
  11765. if (data && data.dot && (data.nodes || data.edges)) {
  11766. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  11767. ' parameter pair "nodes" and "edges", but not both.');
  11768. }
  11769. // set options
  11770. this.setOptions(data && data.options);
  11771. // set all data
  11772. if (data && data.dot) {
  11773. // parse DOT file
  11774. if(data && data.dot) {
  11775. var dotData = vis.util.DOTToGraph(data.dot);
  11776. this.setData(dotData);
  11777. return;
  11778. }
  11779. }
  11780. else {
  11781. this._setNodes(data && data.nodes);
  11782. this._setEdges(data && data.edges);
  11783. }
  11784. this._putDataInSector();
  11785. if (!disableStart) {
  11786. // find a stable position or start animating to a stable position
  11787. if (this.stabilize) {
  11788. this._doStabilize();
  11789. }
  11790. this.moving = true;
  11791. this.start();
  11792. }
  11793. };
  11794. /**
  11795. * Set options
  11796. * @param {Object} options
  11797. */
  11798. Graph.prototype.setOptions = function (options) {
  11799. if (options) {
  11800. // retrieve parameter values
  11801. if (options.width !== undefined) {this.width = options.width;}
  11802. if (options.height !== undefined) {this.height = options.height;}
  11803. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  11804. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  11805. if (options.clustering) {
  11806. this.constants.clustering.enabled = true;
  11807. for (var prop in options.clustering) {
  11808. if (options.clustering.hasOwnProperty(prop)) {
  11809. this.constants.clustering[prop] = options.clustering[prop];
  11810. }
  11811. }
  11812. }
  11813. else if (options.clustering !== undefined) {
  11814. this.constants.clustering.enabled = false;
  11815. }
  11816. if (options.navigation) {
  11817. this.constants.navigation.enabled = true;
  11818. for (var prop in options.navigation) {
  11819. if (options.navigation.hasOwnProperty(prop)) {
  11820. this.constants.navigation[prop] = options.navigation[prop];
  11821. }
  11822. }
  11823. }
  11824. else if (options.navigation !== undefined) {
  11825. this.constants.navigation.enabled = false;
  11826. }
  11827. if (options.keyboard) {
  11828. this.constants.keyboard.enabled = true;
  11829. for (var prop in options.keyboard) {
  11830. if (options.keyboard.hasOwnProperty(prop)) {
  11831. this.constants.keyboard[prop] = options.keyboard[prop];
  11832. }
  11833. }
  11834. }
  11835. else if (options.keyboard !== undefined) {
  11836. this.constants.keyboard.enabled = false;
  11837. }
  11838. // TODO: work out these options and document them
  11839. if (options.edges) {
  11840. for (prop in options.edges) {
  11841. if (options.edges.hasOwnProperty(prop)) {
  11842. this.constants.edges[prop] = options.edges[prop];
  11843. }
  11844. }
  11845. if (options.edges.length !== undefined &&
  11846. options.nodes && options.nodes.distance === undefined) {
  11847. this.constants.edges.length = options.edges.length;
  11848. this.constants.nodes.distance = options.edges.length * 1.25;
  11849. }
  11850. if (!options.edges.fontColor) {
  11851. this.constants.edges.fontColor = options.edges.color;
  11852. }
  11853. // Added to support dashed lines
  11854. // David Jordan
  11855. // 2012-08-08
  11856. if (options.edges.dash) {
  11857. if (options.edges.dash.length !== undefined) {
  11858. this.constants.edges.dash.length = options.edges.dash.length;
  11859. }
  11860. if (options.edges.dash.gap !== undefined) {
  11861. this.constants.edges.dash.gap = options.edges.dash.gap;
  11862. }
  11863. if (options.edges.dash.altLength !== undefined) {
  11864. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  11865. }
  11866. }
  11867. }
  11868. if (options.nodes) {
  11869. for (prop in options.nodes) {
  11870. if (options.nodes.hasOwnProperty(prop)) {
  11871. this.constants.nodes[prop] = options.nodes[prop];
  11872. }
  11873. }
  11874. if (options.nodes.color) {
  11875. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  11876. }
  11877. /*
  11878. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  11879. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  11880. */
  11881. }
  11882. if (options.groups) {
  11883. for (var groupname in options.groups) {
  11884. if (options.groups.hasOwnProperty(groupname)) {
  11885. var group = options.groups[groupname];
  11886. this.groups.add(groupname, group);
  11887. }
  11888. }
  11889. }
  11890. }
  11891. this.setSize(this.width, this.height);
  11892. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  11893. this._setScale(1);
  11894. // load the navigation system.
  11895. this._loadNavigationControls();
  11896. // bind keys. If disabled, this will not do anything;
  11897. this._createKeyBinds();
  11898. this._redraw();
  11899. };
  11900. /**
  11901. * fire an event
  11902. * @param {String} event The name of an event, for example 'select'
  11903. * @param {Object} params Optional object with event parameters
  11904. * @private
  11905. */
  11906. Graph.prototype._trigger = function (event, params) {
  11907. events.trigger(this, event, params);
  11908. };
  11909. /**
  11910. * Create the main frame for the Graph.
  11911. * This function is executed once when a Graph object is created. The frame
  11912. * contains a canvas, and this canvas contains all objects like the axis and
  11913. * nodes.
  11914. * @private
  11915. */
  11916. Graph.prototype._create = function () {
  11917. // remove all elements from the container element.
  11918. while (this.containerElement.hasChildNodes()) {
  11919. this.containerElement.removeChild(this.containerElement.firstChild);
  11920. }
  11921. this.frame = document.createElement('div');
  11922. this.frame.className = 'graph-frame';
  11923. this.frame.style.position = 'relative';
  11924. this.frame.style.overflow = 'hidden';
  11925. // create the graph canvas (HTML canvas element)
  11926. this.frame.canvas = document.createElement( 'canvas' );
  11927. this.frame.canvas.style.position = 'relative';
  11928. this.frame.appendChild(this.frame.canvas);
  11929. if (!this.frame.canvas.getContext) {
  11930. var noCanvas = document.createElement( 'DIV' );
  11931. noCanvas.style.color = 'red';
  11932. noCanvas.style.fontWeight = 'bold' ;
  11933. noCanvas.style.padding = '10px';
  11934. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  11935. this.frame.canvas.appendChild(noCanvas);
  11936. }
  11937. var me = this;
  11938. this.drag = {};
  11939. this.pinch = {};
  11940. this.hammer = Hammer(this.frame.canvas, {
  11941. prevent_default: true
  11942. });
  11943. this.hammer.on('tap', me._onTap.bind(me) );
  11944. this.hammer.on('doubletap', me._onDoubleTap.bind(me) );
  11945. this.hammer.on('hold', me._onHold.bind(me) );
  11946. this.hammer.on('pinch', me._onPinch.bind(me) );
  11947. this.hammer.on('touch', me._onTouch.bind(me) );
  11948. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  11949. this.hammer.on('drag', me._onDrag.bind(me) );
  11950. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  11951. this.hammer.on('release', me._onRelease.bind(me) );
  11952. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  11953. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  11954. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  11955. // add the frame to the container element
  11956. this.containerElement.appendChild(this.frame);
  11957. };
  11958. /**
  11959. * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
  11960. * @private
  11961. */
  11962. Graph.prototype._createKeyBinds = function() {
  11963. var me = this;
  11964. this.mousetrap = mousetrap;
  11965. this.mousetrap.reset();
  11966. if (this.constants.keyboard.enabled == true) {
  11967. this.mousetrap.bind("up", this._moveUp.bind(me) , "keydown");
  11968. this.mousetrap.bind("up", this._yStopMoving.bind(me), "keyup");
  11969. this.mousetrap.bind("down", this._moveDown.bind(me) , "keydown");
  11970. this.mousetrap.bind("down", this._yStopMoving.bind(me), "keyup");
  11971. this.mousetrap.bind("left", this._moveLeft.bind(me) , "keydown");
  11972. this.mousetrap.bind("left", this._xStopMoving.bind(me), "keyup");
  11973. this.mousetrap.bind("right",this._moveRight.bind(me), "keydown");
  11974. this.mousetrap.bind("right",this._xStopMoving.bind(me), "keyup");
  11975. this.mousetrap.bind("=", this._zoomIn.bind(me), "keydown");
  11976. this.mousetrap.bind("=", this._stopZoom.bind(me), "keyup");
  11977. this.mousetrap.bind("-", this._zoomOut.bind(me), "keydown");
  11978. this.mousetrap.bind("-", this._stopZoom.bind(me), "keyup");
  11979. this.mousetrap.bind("[", this._zoomIn.bind(me), "keydown");
  11980. this.mousetrap.bind("[", this._stopZoom.bind(me), "keyup");
  11981. this.mousetrap.bind("]", this._zoomOut.bind(me), "keydown");
  11982. this.mousetrap.bind("]", this._stopZoom.bind(me), "keyup");
  11983. this.mousetrap.bind("pageup",this._zoomIn.bind(me), "keydown");
  11984. this.mousetrap.bind("pageup",this._stopZoom.bind(me), "keyup");
  11985. this.mousetrap.bind("pagedown",this._zoomOut.bind(me),"keydown");
  11986. this.mousetrap.bind("pagedown",this._stopZoom.bind(me), "keyup");
  11987. }
  11988. /*
  11989. this.mousetrap.bind("=",this.decreaseClusterLevel.bind(me));
  11990. this.mousetrap.bind("-",this.increaseClusterLevel.bind(me));
  11991. this.mousetrap.bind("s",this.singleStep.bind(me));
  11992. this.mousetrap.bind("h",this.updateClustersDefault.bind(me));
  11993. this.mousetrap.bind("c",this._collapseSector.bind(me));
  11994. this.mousetrap.bind("f",this.toggleFreeze.bind(me));
  11995. */
  11996. }
  11997. /**
  11998. * Get the pointer location from a touch location
  11999. * @param {{pageX: Number, pageY: Number}} touch
  12000. * @return {{x: Number, y: Number}} pointer
  12001. * @private
  12002. */
  12003. Graph.prototype._getPointer = function (touch) {
  12004. return {
  12005. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  12006. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  12007. };
  12008. };
  12009. /**
  12010. * On start of a touch gesture, store the pointer
  12011. * @param event
  12012. * @private
  12013. */
  12014. Graph.prototype._onTouch = function (event) {
  12015. this.drag.pointer = this._getPointer(event.gesture.touches[0]);
  12016. this.drag.pinched = false;
  12017. this.pinch.scale = this._getScale();
  12018. this._handleTouch(this.drag.pointer);
  12019. };
  12020. /**
  12021. * handle drag start event
  12022. * @private
  12023. */
  12024. Graph.prototype._onDragStart = function () {
  12025. var drag = this.drag;
  12026. var node = this._getNodeAt(drag.pointer);
  12027. // note: drag.pointer is set in _onTouch to get the initial touch location
  12028. drag.selection = [];
  12029. drag.translation = this._getTranslation();
  12030. drag.nodeId = null;
  12031. if (node != null) {
  12032. drag.nodeId = node.id;
  12033. // select the clicked node if not yet selected
  12034. if (!node.isSelected()) {
  12035. this._selectNode(node,false);
  12036. }
  12037. // create an array with the selected nodes and their original location and status
  12038. var me = this;
  12039. this.selection.forEach(function (id) {
  12040. var node = me.nodes[id];
  12041. if (node) {
  12042. var s = {
  12043. id: id,
  12044. node: node,
  12045. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  12046. x: node.x,
  12047. y: node.y,
  12048. xFixed: node.xFixed,
  12049. yFixed: node.yFixed
  12050. };
  12051. node.xFixed = true;
  12052. node.yFixed = true;
  12053. drag.selection.push(s);
  12054. }
  12055. });
  12056. }
  12057. };
  12058. /**
  12059. * handle drag event
  12060. * @private
  12061. */
  12062. Graph.prototype._onDrag = function (event) {
  12063. if (this.drag.pinched) {
  12064. return;
  12065. }
  12066. var pointer = this._getPointer(event.gesture.touches[0]);
  12067. var me = this,
  12068. drag = this.drag,
  12069. selection = drag.selection;
  12070. if (selection && selection.length) {
  12071. // calculate delta's and new location
  12072. var deltaX = pointer.x - drag.pointer.x,
  12073. deltaY = pointer.y - drag.pointer.y;
  12074. // update position of all selected nodes
  12075. selection.forEach(function (s) {
  12076. var node = s.node;
  12077. if (!s.xFixed) {
  12078. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  12079. }
  12080. if (!s.yFixed) {
  12081. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  12082. }
  12083. });
  12084. // start animation if not yet running
  12085. if (!this.moving) {
  12086. this.moving = true;
  12087. this.start();
  12088. }
  12089. }
  12090. else {
  12091. // move the graph
  12092. var diffX = pointer.x - this.drag.pointer.x;
  12093. var diffY = pointer.y - this.drag.pointer.y;
  12094. this._setTranslation(
  12095. this.drag.translation.x + diffX,
  12096. this.drag.translation.y + diffY);
  12097. this._redraw();
  12098. this.moved = true;
  12099. }
  12100. };
  12101. /**
  12102. * handle drag start event
  12103. * @private
  12104. */
  12105. Graph.prototype._onDragEnd = function () {
  12106. var selection = this.drag.selection;
  12107. if (selection) {
  12108. selection.forEach(function (s) {
  12109. // restore original xFixed and yFixed
  12110. s.node.xFixed = s.xFixed;
  12111. s.node.yFixed = s.yFixed;
  12112. });
  12113. }
  12114. };
  12115. /**
  12116. * handle tap/click event: select/unselect a node
  12117. * @private
  12118. */
  12119. Graph.prototype._onTap = function (event) {
  12120. var pointer = this._getPointer(event.gesture.touches[0]);
  12121. this._handleTap(pointer);
  12122. };
  12123. /**
  12124. * handle doubletap event
  12125. * @private
  12126. */
  12127. Graph.prototype._onDoubleTap = function (event) {
  12128. var pointer = this._getPointer(event.gesture.touches[0]);
  12129. this._handleDoubleTap(pointer);
  12130. };
  12131. /**
  12132. * handle long tap event: multi select nodes
  12133. * @private
  12134. */
  12135. Graph.prototype._onHold = function (event) {
  12136. var pointer = this._getPointer(event.gesture.touches[0]);
  12137. this._handleOnHold(pointer);
  12138. };
  12139. /**
  12140. * handle the release of the screen
  12141. *
  12142. * @param event
  12143. * @private
  12144. */
  12145. Graph.prototype._onRelease = function (event) {
  12146. this._handleOnRelease();
  12147. };
  12148. /**
  12149. * Handle pinch event
  12150. * @param event
  12151. * @private
  12152. */
  12153. Graph.prototype._onPinch = function (event) {
  12154. var pointer = this._getPointer(event.gesture.center);
  12155. this.drag.pinched = true;
  12156. if (!('scale' in this.pinch)) {
  12157. this.pinch.scale = 1;
  12158. }
  12159. // TODO: enabled moving while pinching?
  12160. var scale = this.pinch.scale * event.gesture.scale;
  12161. this._zoom(scale, pointer)
  12162. };
  12163. /**
  12164. * Zoom the graph in or out
  12165. * @param {Number} scale a number around 1, and between 0.01 and 10
  12166. * @param {{x: Number, y: Number}} pointer
  12167. * @return {Number} appliedScale scale is limited within the boundaries
  12168. * @private
  12169. */
  12170. Graph.prototype._zoom = function(scale, pointer) {
  12171. var scaleOld = this._getScale();
  12172. if (scale < 0.00001) {
  12173. scale = 0.00001;
  12174. }
  12175. if (scale > 10) {
  12176. scale = 10;
  12177. }
  12178. // + this.frame.canvas.clientHeight / 2
  12179. var translation = this._getTranslation();
  12180. var scaleFrac = scale / scaleOld;
  12181. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  12182. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  12183. this.areaCenter = {"x" : this._canvasToX(pointer.x),
  12184. "y" : this._canvasToY(pointer.y)};
  12185. // this.areaCenter = {"x" : pointer.x,"y" : pointer.y };
  12186. // console.log(translation.x,translation.y,pointer.x,pointer.y,scale);
  12187. this.pinch.mousewheelScale = scale;
  12188. this._setScale(scale);
  12189. this._setTranslation(tx, ty);
  12190. this.updateClustersDefault();
  12191. this._redraw();
  12192. return scale;
  12193. };
  12194. /**
  12195. * Event handler for mouse wheel event, used to zoom the timeline
  12196. * See http://adomas.org/javascript-mouse-wheel/
  12197. * https://github.com/EightMedia/hammer.js/issues/256
  12198. * @param {MouseEvent} event
  12199. * @private
  12200. */
  12201. Graph.prototype._onMouseWheel = function(event) {
  12202. // retrieve delta
  12203. var delta = 0;
  12204. if (event.wheelDelta) { /* IE/Opera. */
  12205. delta = event.wheelDelta/120;
  12206. } else if (event.detail) { /* Mozilla case. */
  12207. // In Mozilla, sign of delta is different than in IE.
  12208. // Also, delta is multiple of 3.
  12209. delta = -event.detail/3;
  12210. }
  12211. // If delta is nonzero, handle it.
  12212. // Basically, delta is now positive if wheel was scrolled up,
  12213. // and negative, if wheel was scrolled down.
  12214. if (delta) {
  12215. if (!('mousewheelScale' in this.pinch)) {
  12216. this.pinch.mousewheelScale = 1;
  12217. }
  12218. // calculate the new scale
  12219. var scale = this.pinch.mousewheelScale;
  12220. var zoom = delta / 10;
  12221. if (delta < 0) {
  12222. zoom = zoom / (1 - zoom);
  12223. }
  12224. scale *= (1 + zoom);
  12225. // calculate the pointer location
  12226. var gesture = util.fakeGesture(this, event);
  12227. var pointer = this._getPointer(gesture.center);
  12228. // apply the new scale
  12229. scale = this._zoom(scale, pointer);
  12230. // store the new, applied scale -- this is now done in _zoom
  12231. // this.pinch.mousewheelScale = scale;
  12232. }
  12233. // Prevent default actions caused by mouse wheel.
  12234. event.preventDefault();
  12235. };
  12236. /**
  12237. * Mouse move handler for checking whether the title moves over a node with a title.
  12238. * @param {Event} event
  12239. * @private
  12240. */
  12241. Graph.prototype._onMouseMoveTitle = function (event) {
  12242. var gesture = util.fakeGesture(this, event);
  12243. var pointer = this._getPointer(gesture.center);
  12244. this.lastPointerPosition = pointer;
  12245. // check if the previously selected node is still selected
  12246. if (this.popupNode) {
  12247. this._checkHidePopup(pointer);
  12248. }
  12249. // start a timeout that will check if the mouse is positioned above
  12250. // an element
  12251. var me = this;
  12252. var checkShow = function() {
  12253. me._checkShowPopup(pointer);
  12254. };
  12255. if (this.popupTimer) {
  12256. clearInterval(this.popupTimer); // stop any running calculationTimer
  12257. }
  12258. if (!this.leftButtonDown) {
  12259. this.popupTimer = setTimeout(checkShow, 300);
  12260. }
  12261. };
  12262. /**
  12263. * Check if there is an element on the given position in the graph
  12264. * (a node or edge). If so, and if this element has a title,
  12265. * show a popup window with its title.
  12266. *
  12267. * @param {{x:Number, y:Number}} pointer
  12268. * @private
  12269. */
  12270. Graph.prototype._checkShowPopup = function (pointer) {
  12271. var obj = {
  12272. left: this._canvasToX(pointer.x),
  12273. top: this._canvasToY(pointer.y),
  12274. right: this._canvasToX(pointer.x),
  12275. bottom: this._canvasToY(pointer.y)
  12276. };
  12277. var id;
  12278. var lastPopupNode = this.popupNode;
  12279. if (this.popupNode == undefined) {
  12280. // search the nodes for overlap, select the top one in case of multiple nodes
  12281. var nodes = this.nodes;
  12282. for (id in nodes) {
  12283. if (nodes.hasOwnProperty(id)) {
  12284. var node = nodes[id];
  12285. if (node.getTitle() !== undefined && node.isOverlappingWith(obj)) {
  12286. this.popupNode = node;
  12287. break;
  12288. }
  12289. }
  12290. }
  12291. }
  12292. if (this.popupNode === undefined) {
  12293. // search the edges for overlap
  12294. var edges = this.edges;
  12295. for (id in edges) {
  12296. if (edges.hasOwnProperty(id)) {
  12297. var edge = edges[id];
  12298. if (edge.connected && (edge.getTitle() !== undefined) &&
  12299. edge.isOverlappingWith(obj)) {
  12300. this.popupNode = edge;
  12301. break;
  12302. }
  12303. }
  12304. }
  12305. }
  12306. if (this.popupNode) {
  12307. // show popup message window
  12308. if (this.popupNode != lastPopupNode) {
  12309. var me = this;
  12310. if (!me.popup) {
  12311. me.popup = new Popup(me.frame);
  12312. }
  12313. // adjust a small offset such that the mouse cursor is located in the
  12314. // bottom left location of the popup, and you can easily move over the
  12315. // popup area
  12316. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  12317. me.popup.setText(me.popupNode.getTitle());
  12318. me.popup.show();
  12319. }
  12320. }
  12321. else {
  12322. if (this.popup) {
  12323. this.popup.hide();
  12324. }
  12325. }
  12326. };
  12327. /**
  12328. * Check if the popup must be hided, which is the case when the mouse is no
  12329. * longer hovering on the object
  12330. * @param {{x:Number, y:Number}} pointer
  12331. * @private
  12332. */
  12333. Graph.prototype._checkHidePopup = function (pointer) {
  12334. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  12335. this.popupNode = undefined;
  12336. if (this.popup) {
  12337. this.popup.hide();
  12338. }
  12339. }
  12340. };
  12341. /**
  12342. * Temporary method to test calculating a hub value for the nodes
  12343. * @param {number} level Maximum number edges between two nodes in order
  12344. * to call them connected. Optional, 1 by default
  12345. * @return {Number[]} connectioncount array with the connection count
  12346. * for each node
  12347. * @private
  12348. */
  12349. Graph.prototype._getConnectionCount = function(level) {
  12350. if (level == undefined) {
  12351. level = 1;
  12352. }
  12353. // get the nodes connected to given nodes
  12354. function getConnectedNodes(nodes) {
  12355. var connectedNodes = [];
  12356. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  12357. var node = nodes[j];
  12358. // find all nodes connected to this node
  12359. var edges = node.edges;
  12360. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  12361. var edge = edges[i];
  12362. var other = null;
  12363. // check if connected
  12364. if (edge.from == node)
  12365. other = edge.to;
  12366. else if (edge.to == node)
  12367. other = edge.from;
  12368. // check if the other node is not already in the list with nodes
  12369. var k, kMax;
  12370. if (other) {
  12371. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  12372. if (nodes[k] == other) {
  12373. other = null;
  12374. break;
  12375. }
  12376. }
  12377. }
  12378. if (other) {
  12379. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  12380. if (connectedNodes[k] == other) {
  12381. other = null;
  12382. break;
  12383. }
  12384. }
  12385. }
  12386. if (other)
  12387. connectedNodes.push(other);
  12388. }
  12389. }
  12390. return connectedNodes;
  12391. }
  12392. var connections = [];
  12393. var nodes = this.nodes;
  12394. for (var id in nodes) {
  12395. if (nodes.hasOwnProperty(id)) {
  12396. var c = [nodes[id]];
  12397. for (var l = 0; l < level; l++) {
  12398. c = c.concat(getConnectedNodes(c));
  12399. }
  12400. connections.push(c);
  12401. }
  12402. }
  12403. var hubs = [];
  12404. for (var i = 0, len = connections.length; i < len; i++) {
  12405. hubs.push(connections[i].length);
  12406. }
  12407. return hubs;
  12408. };
  12409. /**
  12410. * Set a new size for the graph
  12411. * @param {string} width Width in pixels or percentage (for example '800px'
  12412. * or '50%')
  12413. * @param {string} height Height in pixels or percentage (for example '400px'
  12414. * or '30%')
  12415. */
  12416. Graph.prototype.setSize = function(width, height) {
  12417. this.frame.style.width = width;
  12418. this.frame.style.height = height;
  12419. this.frame.canvas.style.width = '100%';
  12420. this.frame.canvas.style.height = '100%';
  12421. this.frame.canvas.width = this.frame.canvas.clientWidth;
  12422. this.frame.canvas.height = this.frame.canvas.clientHeight;
  12423. if (this.constants.navigation.enabled == true) {
  12424. this._relocateNavigation();
  12425. }
  12426. };
  12427. /**
  12428. * Set a data set with nodes for the graph
  12429. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  12430. * @private
  12431. */
  12432. Graph.prototype._setNodes = function(nodes) {
  12433. var oldNodesData = this.nodesData;
  12434. if (nodes instanceof DataSet || nodes instanceof DataView) {
  12435. this.nodesData = nodes;
  12436. }
  12437. else if (nodes instanceof Array) {
  12438. this.nodesData = new DataSet();
  12439. this.nodesData.add(nodes);
  12440. }
  12441. else if (!nodes) {
  12442. this.nodesData = new DataSet();
  12443. }
  12444. else {
  12445. throw new TypeError('Array or DataSet expected');
  12446. }
  12447. if (oldNodesData) {
  12448. // unsubscribe from old dataset
  12449. util.forEach(this.nodesListeners, function (callback, event) {
  12450. oldNodesData.unsubscribe(event, callback);
  12451. });
  12452. }
  12453. // remove drawn nodes
  12454. this.nodes = {};
  12455. if (this.nodesData) {
  12456. // subscribe to new dataset
  12457. var me = this;
  12458. util.forEach(this.nodesListeners, function (callback, event) {
  12459. me.nodesData.subscribe(event, callback);
  12460. });
  12461. // draw all new nodes
  12462. var ids = this.nodesData.getIds();
  12463. this._addNodes(ids);
  12464. }
  12465. this._updateSelection();
  12466. };
  12467. /**
  12468. * Add nodes
  12469. * @param {Number[] | String[]} ids
  12470. * @private
  12471. */
  12472. Graph.prototype._addNodes = function(ids) {
  12473. var id;
  12474. for (var i = 0, len = ids.length; i < len; i++) {
  12475. id = ids[i];
  12476. var data = this.nodesData.get(id);
  12477. var node = new Node(data, this.images, this.groups, this.constants);
  12478. this.nodes[id] = node; // note: this may replace an existing node
  12479. if (!node.isFixed()) {
  12480. // TODO: position new nodes in a smarter way!
  12481. var radius = this.constants.edges.length * 2;
  12482. var count = ids.length;
  12483. var angle = 2 * Math.PI * (i / count);
  12484. node.x = radius * Math.cos(angle);
  12485. node.y = radius * Math.sin(angle);
  12486. // note: no not use node.isMoving() here, as that gives the current
  12487. // velocity of the node, which is zero after creation of the node.
  12488. this.moving = true;
  12489. }
  12490. }
  12491. this._updateNodeIndexList();
  12492. this._reconnectEdges();
  12493. this._updateValueRange(this.nodes);
  12494. };
  12495. /**
  12496. * Update existing nodes, or create them when not yet existing
  12497. * @param {Number[] | String[]} ids
  12498. * @private
  12499. */
  12500. Graph.prototype._updateNodes = function(ids) {
  12501. var nodes = this.nodes,
  12502. nodesData = this.nodesData;
  12503. for (var i = 0, len = ids.length; i < len; i++) {
  12504. var id = ids[i];
  12505. var node = nodes[id];
  12506. var data = nodesData.get(id);
  12507. if (node) {
  12508. // update node
  12509. node.setProperties(data, this.constants);
  12510. }
  12511. else {
  12512. // create node
  12513. node = new Node(properties, this.images, this.groups, this.constants);
  12514. nodes[id] = node;
  12515. if (!node.isFixed()) {
  12516. this.moving = true;
  12517. }
  12518. }
  12519. }
  12520. this._updateNodeIndexList();
  12521. this._reconnectEdges();
  12522. this._updateValueRange(nodes);
  12523. };
  12524. /**
  12525. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  12526. * @param {Number[] | String[]} ids
  12527. * @private
  12528. */
  12529. Graph.prototype._removeNodes = function(ids) {
  12530. var nodes = this.nodes;
  12531. for (var i = 0, len = ids.length; i < len; i++) {
  12532. var id = ids[i];
  12533. delete nodes[id];
  12534. }
  12535. this._updateNodeIndexList();
  12536. this._reconnectEdges();
  12537. this._updateSelection();
  12538. this._updateValueRange(nodes);
  12539. };
  12540. /**
  12541. * Load edges by reading the data table
  12542. * @param {Array | DataSet | DataView} edges The data containing the edges.
  12543. * @private
  12544. * @private
  12545. */
  12546. Graph.prototype._setEdges = function(edges) {
  12547. var oldEdgesData = this.edgesData;
  12548. if (edges instanceof DataSet || edges instanceof DataView) {
  12549. this.edgesData = edges;
  12550. }
  12551. else if (edges instanceof Array) {
  12552. this.edgesData = new DataSet();
  12553. this.edgesData.add(edges);
  12554. }
  12555. else if (!edges) {
  12556. this.edgesData = new DataSet();
  12557. }
  12558. else {
  12559. throw new TypeError('Array or DataSet expected');
  12560. }
  12561. if (oldEdgesData) {
  12562. // unsubscribe from old dataset
  12563. util.forEach(this.edgesListeners, function (callback, event) {
  12564. oldEdgesData.unsubscribe(event, callback);
  12565. });
  12566. }
  12567. // remove drawn edges
  12568. this.edges = {};
  12569. if (this.edgesData) {
  12570. // subscribe to new dataset
  12571. var me = this;
  12572. util.forEach(this.edgesListeners, function (callback, event) {
  12573. me.edgesData.subscribe(event, callback);
  12574. });
  12575. // draw all new nodes
  12576. var ids = this.edgesData.getIds();
  12577. this._addEdges(ids);
  12578. }
  12579. this._reconnectEdges();
  12580. };
  12581. /**
  12582. * Add edges
  12583. * @param {Number[] | String[]} ids
  12584. * @private
  12585. */
  12586. Graph.prototype._addEdges = function (ids) {
  12587. var edges = this.edges,
  12588. edgesData = this.edgesData;
  12589. for (var i = 0, len = ids.length; i < len; i++) {
  12590. var id = ids[i];
  12591. var oldEdge = edges[id];
  12592. if (oldEdge) {
  12593. oldEdge.disconnect();
  12594. }
  12595. var data = edgesData.get(id, {"showInternalIds" : true});
  12596. edges[id] = new Edge(data, this, this.constants);
  12597. }
  12598. this.moving = true;
  12599. this._updateValueRange(edges);
  12600. };
  12601. /**
  12602. * Update existing edges, or create them when not yet existing
  12603. * @param {Number[] | String[]} ids
  12604. * @private
  12605. */
  12606. Graph.prototype._updateEdges = function (ids) {
  12607. var edges = this.edges,
  12608. edgesData = this.edgesData;
  12609. for (var i = 0, len = ids.length; i < len; i++) {
  12610. var id = ids[i];
  12611. var data = edgesData.get(id);
  12612. var edge = edges[id];
  12613. if (edge) {
  12614. // update edge
  12615. edge.disconnect();
  12616. edge.setProperties(data, this.constants);
  12617. edge.connect();
  12618. }
  12619. else {
  12620. // create edge
  12621. edge = new Edge(data, this, this.constants);
  12622. this.edges[id] = edge;
  12623. }
  12624. }
  12625. this.moving = true;
  12626. this._updateValueRange(edges);
  12627. };
  12628. /**
  12629. * Remove existing edges. Non existing ids will be ignored
  12630. * @param {Number[] | String[]} ids
  12631. * @private
  12632. */
  12633. Graph.prototype._removeEdges = function (ids) {
  12634. var edges = this.edges;
  12635. for (var i = 0, len = ids.length; i < len; i++) {
  12636. var id = ids[i];
  12637. var edge = edges[id];
  12638. if (edge) {
  12639. edge.disconnect();
  12640. delete edges[id];
  12641. }
  12642. }
  12643. this.moving = true;
  12644. this._updateValueRange(edges);
  12645. };
  12646. /**
  12647. * Reconnect all edges
  12648. * @private
  12649. */
  12650. Graph.prototype._reconnectEdges = function() {
  12651. var id,
  12652. nodes = this.nodes,
  12653. edges = this.edges;
  12654. for (id in nodes) {
  12655. if (nodes.hasOwnProperty(id)) {
  12656. nodes[id].edges = [];
  12657. }
  12658. }
  12659. for (id in edges) {
  12660. if (edges.hasOwnProperty(id)) {
  12661. var edge = edges[id];
  12662. edge.from = null;
  12663. edge.to = null;
  12664. edge.connect();
  12665. }
  12666. }
  12667. };
  12668. /**
  12669. * Update the values of all object in the given array according to the current
  12670. * value range of the objects in the array.
  12671. * @param {Object} obj An object containing a set of Edges or Nodes
  12672. * The objects must have a method getValue() and
  12673. * setValueRange(min, max).
  12674. * @private
  12675. */
  12676. Graph.prototype._updateValueRange = function(obj) {
  12677. var id;
  12678. // determine the range of the objects
  12679. var valueMin = undefined;
  12680. var valueMax = undefined;
  12681. for (id in obj) {
  12682. if (obj.hasOwnProperty(id)) {
  12683. var value = obj[id].getValue();
  12684. if (value !== undefined) {
  12685. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  12686. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  12687. }
  12688. }
  12689. }
  12690. // adjust the range of all objects
  12691. if (valueMin !== undefined && valueMax !== undefined) {
  12692. for (id in obj) {
  12693. if (obj.hasOwnProperty(id)) {
  12694. obj[id].setValueRange(valueMin, valueMax);
  12695. }
  12696. }
  12697. }
  12698. };
  12699. /**
  12700. * Redraw the graph with the current data
  12701. * chart will be resized too.
  12702. */
  12703. Graph.prototype.redraw = function() {
  12704. this.setSize(this.width, this.height);
  12705. this._redraw();
  12706. };
  12707. /**
  12708. * Redraw the graph with the current data
  12709. * @private
  12710. */
  12711. Graph.prototype._redraw = function() {
  12712. var ctx = this.frame.canvas.getContext('2d');
  12713. // clear the canvas
  12714. var w = this.frame.canvas.width;
  12715. var h = this.frame.canvas.height;
  12716. ctx.clearRect(0, 0, w, h);
  12717. // set scaling and translation
  12718. ctx.save();
  12719. ctx.translate(this.translation.x, this.translation.y);
  12720. ctx.scale(this.scale, this.scale);
  12721. this.canvasTopLeft = {"x": this._canvasToX(0),
  12722. "y": this._canvasToY(0)};
  12723. this.canvasBottomRight = {"x": this._canvasToX(this.frame.canvas.clientWidth),
  12724. "y": this._canvasToY(this.frame.canvas.clientHeight)};
  12725. this._doInAllSectors("_drawAllSectorNodes",ctx);
  12726. this._doInAllSectors("_drawEdges",ctx);
  12727. this._doInAllSectors("_drawNodes",ctx);
  12728. // restore original scaling and translation
  12729. ctx.restore();
  12730. if (this.constants.navigation.enabled == true) {
  12731. this._doInNavigationSector("_drawNodes",ctx,true);
  12732. }
  12733. };
  12734. /**
  12735. * Set the translation of the graph
  12736. * @param {Number} offsetX Horizontal offset
  12737. * @param {Number} offsetY Vertical offset
  12738. * @private
  12739. */
  12740. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  12741. if (this.translation === undefined) {
  12742. this.translation = {
  12743. x: 0,
  12744. y: 0
  12745. };
  12746. }
  12747. if (offsetX !== undefined) {
  12748. this.translation.x = offsetX;
  12749. }
  12750. if (offsetY !== undefined) {
  12751. this.translation.y = offsetY;
  12752. }
  12753. };
  12754. /**
  12755. * Get the translation of the graph
  12756. * @return {Object} translation An object with parameters x and y, both a number
  12757. * @private
  12758. */
  12759. Graph.prototype._getTranslation = function() {
  12760. return {
  12761. x: this.translation.x,
  12762. y: this.translation.y
  12763. };
  12764. };
  12765. /**
  12766. * Scale the graph
  12767. * @param {Number} scale Scaling factor 1.0 is unscaled
  12768. * @private
  12769. */
  12770. Graph.prototype._setScale = function(scale) {
  12771. this.scale = scale;
  12772. };
  12773. /**
  12774. * Get the current scale of the graph
  12775. * @return {Number} scale Scaling factor 1.0 is unscaled
  12776. * @private
  12777. */
  12778. Graph.prototype._getScale = function() {
  12779. return this.scale;
  12780. };
  12781. /**
  12782. * Convert a horizontal point on the HTML canvas to the x-value of the model
  12783. * @param {number} x
  12784. * @returns {number}
  12785. * @private
  12786. */
  12787. Graph.prototype._canvasToX = function(x) {
  12788. return (x - this.translation.x) / this.scale;
  12789. };
  12790. /**
  12791. * Convert an x-value in the model to a horizontal point on the HTML canvas
  12792. * @param {number} x
  12793. * @returns {number}
  12794. * @private
  12795. */
  12796. Graph.prototype._xToCanvas = function(x) {
  12797. return x * this.scale + this.translation.x;
  12798. };
  12799. /**
  12800. * Convert a vertical point on the HTML canvas to the y-value of the model
  12801. * @param {number} y
  12802. * @returns {number}
  12803. * @private
  12804. */
  12805. Graph.prototype._canvasToY = function(y) {
  12806. return (y - this.translation.y) / this.scale;
  12807. };
  12808. /**
  12809. * Convert an y-value in the model to a vertical point on the HTML canvas
  12810. * @param {number} y
  12811. * @returns {number}
  12812. * @private
  12813. */
  12814. Graph.prototype._yToCanvas = function(y) {
  12815. return y * this.scale + this.translation.y ;
  12816. };
  12817. /**
  12818. * Redraw all nodes
  12819. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  12820. * @param {CanvasRenderingContext2D} ctx
  12821. * @param {Boolean} [alwaysShow]
  12822. * @private
  12823. */
  12824. Graph.prototype._drawNodes = function(ctx,alwaysShow) {
  12825. if (alwaysShow === undefined) {
  12826. alwaysShow = false;
  12827. }
  12828. // first draw the unselected nodes
  12829. var nodes = this.nodes;
  12830. var selected = [];
  12831. for (var id in nodes) {
  12832. if (nodes.hasOwnProperty(id)) {
  12833. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  12834. if (nodes[id].isSelected()) {
  12835. selected.push(id);
  12836. }
  12837. else {
  12838. if (nodes[id].inArea() || alwaysShow) {
  12839. nodes[id].draw(ctx);
  12840. }
  12841. }
  12842. }
  12843. }
  12844. // draw the selected nodes on top
  12845. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  12846. if (nodes[selected[s]].inArea() || alwaysShow) {
  12847. nodes[selected[s]].draw(ctx);
  12848. }
  12849. }
  12850. };
  12851. /**
  12852. * Redraw all edges
  12853. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  12854. * @param {CanvasRenderingContext2D} ctx
  12855. * @private
  12856. */
  12857. Graph.prototype._drawEdges = function(ctx) {
  12858. var edges = this.edges;
  12859. for (var id in edges) {
  12860. if (edges.hasOwnProperty(id)) {
  12861. var edge = edges[id];
  12862. edge.setScale(this.scale);
  12863. if (edge.connected) {
  12864. edges[id].draw(ctx);
  12865. }
  12866. }
  12867. }
  12868. };
  12869. /**
  12870. * Find a stable position for all nodes
  12871. * @private
  12872. */
  12873. Graph.prototype._doStabilize = function() {
  12874. //var start = new Date();
  12875. // find stable position
  12876. var count = 0;
  12877. var vmin = this.constants.minVelocity;
  12878. var stable = false;
  12879. while (!stable && count < this.constants.maxIterations) {
  12880. this._initializeForceCalculation();
  12881. this._discreteStepNodes();
  12882. stable = !this._isMoving(vmin);
  12883. count++;
  12884. }
  12885. this.zoomToFit();
  12886. // var end = new Date();
  12887. // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
  12888. };
  12889. /**
  12890. * Before calculating the forces, we check if we need to cluster to keep up performance and we check
  12891. * if there is more than one node. If it is just one node, we dont calculate anything.
  12892. *
  12893. * @private
  12894. */
  12895. Graph.prototype._initializeForceCalculation = function() {
  12896. // stop calculation if there is only one node
  12897. if (this.nodeIndices.length == 1) {
  12898. this.nodes[this.nodeIndices[0]]._setForce(0,0);
  12899. }
  12900. else {
  12901. // if there are too many nodes on screen, we cluster without repositioning
  12902. if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
  12903. this.clusterToFit(this.constants.clustering.reduceToNodes, false);
  12904. }
  12905. // we now start the force calculation
  12906. this._calculateForces();
  12907. }
  12908. };
  12909. /**
  12910. * Calculate the external forces acting on the nodes
  12911. * Forces are caused by: edges, repulsing forces between nodes, gravity
  12912. * @private
  12913. */
  12914. Graph.prototype._calculateForces = function() {
  12915. // var screenCenterPos = {"x":(0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x)),
  12916. // "y":(0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y))}
  12917. // create a local edge to the nodes and edges, that is faster
  12918. var dx, dy, angle, distance, fx, fy,
  12919. repulsingForce, springForce, length, edgeLength,
  12920. node, node1, node2, edge, edgeId, i, j, nodeId, xCenter, yCenter;
  12921. var clusterSize;
  12922. var nodes = this.nodes;
  12923. var edges = this.edges;
  12924. // Gravity is required to keep separated groups from floating off
  12925. // the forces are reset to zero in this loop by using _setForce instead
  12926. // of _addForce
  12927. var gravity = 0.08 * this.forceFactor;
  12928. for (i = 0; i < this.nodeIndices.length; i++) {
  12929. node = nodes[this.nodeIndices[i]];
  12930. // gravity does not apply when we are in a pocket sector
  12931. if (this._sector() == "default") {
  12932. dx = -node.x;// + screenCenterPos.x;
  12933. dy = -node.y;// + screenCenterPos.y;
  12934. angle = Math.atan2(dy, dx);
  12935. fx = Math.cos(angle) * gravity;
  12936. fy = Math.sin(angle) * gravity;
  12937. }
  12938. else {
  12939. fx = 0;
  12940. fy = 0;
  12941. }
  12942. node._setForce(fx, fy);
  12943. node.updateDamping(this.nodeIndices.length);
  12944. }
  12945. // repulsing forces between nodes
  12946. var minimumDistance = this.constants.nodes.distance,
  12947. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  12948. // we loop from i over all but the last entree in the array
  12949. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  12950. for (i = 0; i < this.nodeIndices.length-1; i++) {
  12951. node1 = nodes[this.nodeIndices[i]];
  12952. for (j = i+1; j < this.nodeIndices.length; j++) {
  12953. node2 = nodes[this.nodeIndices[j]];
  12954. clusterSize = (node1.clusterSize + node2.clusterSize - 2);
  12955. dx = node2.x - node1.x;
  12956. dy = node2.y - node1.y;
  12957. distance = Math.sqrt(dx * dx + dy * dy);
  12958. // clusters have a larger region of influence
  12959. minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
  12960. if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
  12961. angle = Math.atan2(dy, dx);
  12962. if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
  12963. repulsingForce = 1.0;
  12964. }
  12965. else {
  12966. // TODO: correct factor for repulsing force
  12967. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12968. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12969. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  12970. }
  12971. // amplify the repulsion for clusters.
  12972. repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
  12973. repulsingForce *= this.forceFactor;
  12974. fx = Math.cos(angle) * repulsingForce;
  12975. fy = Math.sin(angle) * repulsingForce ;
  12976. node1._addForce(-fx, -fy);
  12977. node2._addForce(fx, fy);
  12978. }
  12979. }
  12980. }
  12981. /*
  12982. // repulsion of the edges on the nodes and
  12983. for (var nodeId in nodes) {
  12984. if (nodes.hasOwnProperty(nodeId)) {
  12985. node = nodes[nodeId];
  12986. for(var edgeId in edges) {
  12987. if (edges.hasOwnProperty(edgeId)) {
  12988. edge = edges[edgeId];
  12989. // get the center of the edge
  12990. xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
  12991. yCenter = edge.from.y+(edge.to.y - edge.from.y)/2;
  12992. // calculate normally distributed force
  12993. dx = node.x - xCenter;
  12994. dy = node.y - yCenter;
  12995. distance = Math.sqrt(dx * dx + dy * dy);
  12996. if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
  12997. angle = Math.atan2(dy, dx);
  12998. if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
  12999. repulsingForce = 1.0;
  13000. }
  13001. else {
  13002. // TODO: correct factor for repulsing force
  13003. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  13004. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  13005. repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force
  13006. }
  13007. fx = Math.cos(angle) * repulsingForce;
  13008. fy = Math.sin(angle) * repulsingForce;
  13009. node._addForce(fx, fy);
  13010. edge.from._addForce(-fx/2,-fy/2);
  13011. edge.to._addForce(-fx/2,-fy/2);
  13012. }
  13013. }
  13014. }
  13015. }
  13016. }
  13017. */
  13018. // forces caused by the edges, modelled as springs
  13019. for (edgeId in edges) {
  13020. if (edges.hasOwnProperty(edgeId)) {
  13021. edge = edges[edgeId];
  13022. if (edge.connected) {
  13023. // only calculate forces if nodes are in the same sector
  13024. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  13025. clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
  13026. dx = (edge.to.x - edge.from.x);
  13027. dy = (edge.to.y - edge.from.y);
  13028. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  13029. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  13030. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  13031. edgeLength = edge.length;
  13032. // this implies that the edges between big clusters are longer
  13033. edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
  13034. length = Math.sqrt(dx * dx + dy * dy);
  13035. angle = Math.atan2(dy, dx);
  13036. springForce = edge.stiffness * (edgeLength - length) * this.forceFactor;
  13037. fx = Math.cos(angle) * springForce;
  13038. fy = Math.sin(angle) * springForce;
  13039. edge.from._addForce(-fx, -fy);
  13040. edge.to._addForce(fx, fy);
  13041. }
  13042. }
  13043. }
  13044. }
  13045. /*
  13046. // TODO: re-implement repulsion of edges
  13047. // repulsing forces between edges
  13048. var minimumDistance = this.constants.edges.distance,
  13049. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  13050. for (var l = 0; l < edges.length; l++) {
  13051. //Keep distance from other edge centers
  13052. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  13053. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  13054. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  13055. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  13056. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  13057. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  13058. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  13059. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  13060. // calculate normally distributed force
  13061. dx = l2x - lx,
  13062. dy = l2y - ly,
  13063. distance = Math.sqrt(dx * dx + dy * dy),
  13064. angle = Math.atan2(dy, dx),
  13065. // TODO: correct factor for repulsing force
  13066. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  13067. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  13068. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  13069. fx = Math.cos(angle) * repulsingforce,
  13070. fy = Math.sin(angle) * repulsingforce;
  13071. edges[l].from._addForce(-fx, -fy);
  13072. edges[l].to._addForce(-fx, -fy);
  13073. edges[l2].from._addForce(fx, fy);
  13074. edges[l2].to._addForce(fx, fy);
  13075. }
  13076. }
  13077. */
  13078. };
  13079. /**
  13080. * Check if any of the nodes is still moving
  13081. * @param {number} vmin the minimum velocity considered as 'moving'
  13082. * @return {boolean} true if moving, false if non of the nodes is moving
  13083. * @private
  13084. */
  13085. Graph.prototype._isMoving = function(vmin) {
  13086. var vminCorrected = vmin / this.scale;
  13087. var nodes = this.nodes;
  13088. for (var id in nodes) {
  13089. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vminCorrected)) {
  13090. return true;
  13091. }
  13092. }
  13093. return false;
  13094. };
  13095. /**
  13096. * /**
  13097. * Perform one discrete step for all nodes
  13098. *
  13099. * @param interval
  13100. * @private
  13101. */
  13102. Graph.prototype._discreteStepNodes = function() {
  13103. var interval = 0.01;
  13104. var nodes = this.nodes;
  13105. for (var id in nodes) {
  13106. if (nodes.hasOwnProperty(id)) {
  13107. nodes[id].discreteStep(interval);
  13108. }
  13109. }
  13110. var vmin = this.constants.minVelocity;
  13111. this.moving = this._isMoving(vmin);
  13112. };
  13113. /**
  13114. * Start animating nodes and edges
  13115. *
  13116. * @poram {Boolean} runCalculationStep
  13117. */
  13118. Graph.prototype.start = function() {
  13119. if (!this.freezeSimulation) {
  13120. if (this.moving) {
  13121. this._doInAllActiveSectors("_initializeForceCalculation");
  13122. this._doInAllActiveSectors("_discreteStepNodes");
  13123. this._findCenter(this._getRange())
  13124. }
  13125. if (this.moving || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0) {
  13126. // start animation. only start calculationTimer if it is not already running
  13127. if (!this.timer) {
  13128. var graph = this;
  13129. this.timer = window.setTimeout(function () {
  13130. graph.timer = undefined;
  13131. // keyboad movement
  13132. if (graph.xIncrement != 0 || graph.yIncrement != 0) {
  13133. var translation = graph._getTranslation();
  13134. graph._setTranslation(translation.x+graph.xIncrement,translation.y+graph.yIncrement);
  13135. }
  13136. if (graph.zoomIncrement != 0) {
  13137. graph._zoom(graph.scale*(1 + graph.zoomIncrement),graph.lastPointerPosition);
  13138. }
  13139. graph.start();
  13140. graph._redraw();
  13141. //this.end = window.performance.now();
  13142. //this.time = this.end - this.startTime;
  13143. //console.log('refresh time: ' + this.time);
  13144. //this.startTime = window.performance.now();
  13145. }, this.renderTimestep);
  13146. }
  13147. }
  13148. else {
  13149. this._redraw();
  13150. }
  13151. }
  13152. };
  13153. Graph.prototype.singleStep = function() {
  13154. if (this.moving) {
  13155. this._initializeForceCalculation();
  13156. this._discreteStepNodes();
  13157. var vmin = this.constants.minVelocity;
  13158. this.moving = this._isMoving(vmin);
  13159. this._redraw();
  13160. }
  13161. };
  13162. /**
  13163. * Freeze the animation
  13164. */
  13165. Graph.prototype.toggleFreeze = function() {
  13166. if (this.freezeSimulation == false) {
  13167. this.freezeSimulation = true;
  13168. }
  13169. else {
  13170. this.freezeSimulation = false;
  13171. this.start();
  13172. }
  13173. };
  13174. /**
  13175. * Mixin the cluster system and initialize the parameters required.
  13176. *
  13177. * @private
  13178. */
  13179. Graph.prototype._loadClusterSystem = function() {
  13180. this.clusterSession = 0;
  13181. this.hubThreshold = 5;
  13182. for (var mixinFunction in ClusterMixin) {
  13183. if (ClusterMixin.hasOwnProperty(mixinFunction)) {
  13184. Graph.prototype[mixinFunction] = ClusterMixin[mixinFunction];
  13185. }
  13186. }
  13187. }
  13188. /**
  13189. * Mixin the sector system and initialize the parameters required
  13190. *
  13191. * @private
  13192. */
  13193. Graph.prototype._loadSectorSystem = function() {
  13194. this.sectors = {};
  13195. this.activeSector = ["default"];
  13196. this.sectors["active"] = {};
  13197. this.sectors["active"]["default"] = {"nodes":{},
  13198. "edges":{},
  13199. "nodeIndices":[],
  13200. "formationScale": 1.0,
  13201. "drawingNode": undefined};
  13202. this.sectors["frozen"] = {};
  13203. this.sectors["navigation"] = {"nodes":{},
  13204. "edges":{},
  13205. "nodeIndices":[],
  13206. "formationScale": 1.0,
  13207. "drawingNode": undefined};
  13208. this.nodeIndices = this.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  13209. for (var mixinFunction in SectorMixin) {
  13210. if (SectorMixin.hasOwnProperty(mixinFunction)) {
  13211. Graph.prototype[mixinFunction] = SectorMixin[mixinFunction];
  13212. }
  13213. }
  13214. };
  13215. /**
  13216. * Mixin the selection system and initialize the parameters required
  13217. *
  13218. * @private
  13219. */
  13220. Graph.prototype._loadSelectionSystem = function() {
  13221. this.selection = [];
  13222. this.selectionObj = {};
  13223. for (var mixinFunction in SelectionMixin) {
  13224. if (SelectionMixin.hasOwnProperty(mixinFunction)) {
  13225. Graph.prototype[mixinFunction] = SelectionMixin[mixinFunction];
  13226. }
  13227. }
  13228. }
  13229. /**
  13230. * Mixin the navigation (User Interface) system and initialize the parameters required
  13231. *
  13232. * @private
  13233. */
  13234. Graph.prototype._loadNavigationControls = function() {
  13235. for (var mixinFunction in NavigationMixin) {
  13236. if (NavigationMixin.hasOwnProperty(mixinFunction)) {
  13237. Graph.prototype[mixinFunction] = NavigationMixin[mixinFunction];
  13238. }
  13239. }
  13240. if (this.constants.navigation.enabled == true) {
  13241. this._loadNavigationElements();
  13242. }
  13243. }
  13244. /**
  13245. * this function exists to avoid errors when not loading the navigation system
  13246. */
  13247. Graph.prototype._relocateNavigation = function() {
  13248. // empty, is overloaded by navigation system
  13249. }
  13250. /**
  13251. * * this function exists to avoid errors when not loading the navigation system
  13252. */
  13253. Graph.prototype._unHighlightAll = function() {
  13254. // empty, is overloaded by the navigation system
  13255. }
  13256. /**
  13257. * vis.js module exports
  13258. */
  13259. var vis = {
  13260. util: util,
  13261. events: events,
  13262. Controller: Controller,
  13263. DataSet: DataSet,
  13264. DataView: DataView,
  13265. Range: Range,
  13266. Stack: Stack,
  13267. TimeStep: TimeStep,
  13268. EventBus: EventBus,
  13269. components: {
  13270. items: {
  13271. Item: Item,
  13272. ItemBox: ItemBox,
  13273. ItemPoint: ItemPoint,
  13274. ItemRange: ItemRange
  13275. },
  13276. Component: Component,
  13277. Panel: Panel,
  13278. RootPanel: RootPanel,
  13279. ItemSet: ItemSet,
  13280. TimeAxis: TimeAxis
  13281. },
  13282. graph: {
  13283. Node: Node,
  13284. Edge: Edge,
  13285. Popup: Popup,
  13286. Groups: Groups,
  13287. Images: Images
  13288. },
  13289. Timeline: Timeline,
  13290. Graph: Graph
  13291. };
  13292. /**
  13293. * CommonJS module exports
  13294. */
  13295. if (typeof exports !== 'undefined') {
  13296. exports = vis;
  13297. }
  13298. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  13299. module.exports = vis;
  13300. }
  13301. /**
  13302. * AMD module exports
  13303. */
  13304. if (typeof(define) === 'function') {
  13305. define(function () {
  13306. return vis;
  13307. });
  13308. }
  13309. /**
  13310. * Window exports
  13311. */
  13312. if (typeof window !== 'undefined') {
  13313. // attach the module to the window, load as a regular javascript file
  13314. window['vis'] = vis;
  13315. }
  13316. },{"hammerjs":2,"moment":3,"mousetrap":4}],2:[function(require,module,exports){
  13317. /*! Hammer.JS - v1.0.5 - 2013-04-07
  13318. * http://eightmedia.github.com/hammer.js
  13319. *
  13320. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  13321. * Licensed under the MIT license */
  13322. (function(window, undefined) {
  13323. 'use strict';
  13324. /**
  13325. * Hammer
  13326. * use this to create instances
  13327. * @param {HTMLElement} element
  13328. * @param {Object} options
  13329. * @returns {Hammer.Instance}
  13330. * @constructor
  13331. */
  13332. var Hammer = function(element, options) {
  13333. return new Hammer.Instance(element, options || {});
  13334. };
  13335. // default settings
  13336. Hammer.defaults = {
  13337. // add styles and attributes to the element to prevent the browser from doing
  13338. // its native behavior. this doesnt prevent the scrolling, but cancels
  13339. // the contextmenu, tap highlighting etc
  13340. // set to false to disable this
  13341. stop_browser_behavior: {
  13342. // this also triggers onselectstart=false for IE
  13343. userSelect: 'none',
  13344. // this makes the element blocking in IE10 >, you could experiment with the value
  13345. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  13346. touchAction: 'none',
  13347. touchCallout: 'none',
  13348. contentZooming: 'none',
  13349. userDrag: 'none',
  13350. tapHighlightColor: 'rgba(0,0,0,0)'
  13351. }
  13352. // more settings are defined per gesture at gestures.js
  13353. };
  13354. // detect touchevents
  13355. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  13356. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  13357. // dont use mouseevents on mobile devices
  13358. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  13359. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  13360. // eventtypes per touchevent (start, move, end)
  13361. // are filled by Hammer.event.determineEventTypes on setup
  13362. Hammer.EVENT_TYPES = {};
  13363. // direction defines
  13364. Hammer.DIRECTION_DOWN = 'down';
  13365. Hammer.DIRECTION_LEFT = 'left';
  13366. Hammer.DIRECTION_UP = 'up';
  13367. Hammer.DIRECTION_RIGHT = 'right';
  13368. // pointer type
  13369. Hammer.POINTER_MOUSE = 'mouse';
  13370. Hammer.POINTER_TOUCH = 'touch';
  13371. Hammer.POINTER_PEN = 'pen';
  13372. // touch event defines
  13373. Hammer.EVENT_START = 'start';
  13374. Hammer.EVENT_MOVE = 'move';
  13375. Hammer.EVENT_END = 'end';
  13376. // hammer document where the base events are added at
  13377. Hammer.DOCUMENT = document;
  13378. // plugins namespace
  13379. Hammer.plugins = {};
  13380. // if the window events are set...
  13381. Hammer.READY = false;
  13382. /**
  13383. * setup events to detect gestures on the document
  13384. */
  13385. function setup() {
  13386. if(Hammer.READY) {
  13387. return;
  13388. }
  13389. // find what eventtypes we add listeners to
  13390. Hammer.event.determineEventTypes();
  13391. // Register all gestures inside Hammer.gestures
  13392. for(var name in Hammer.gestures) {
  13393. if(Hammer.gestures.hasOwnProperty(name)) {
  13394. Hammer.detection.register(Hammer.gestures[name]);
  13395. }
  13396. }
  13397. // Add touch events on the document
  13398. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  13399. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  13400. // Hammer is ready...!
  13401. Hammer.READY = true;
  13402. }
  13403. /**
  13404. * create new hammer instance
  13405. * all methods should return the instance itself, so it is chainable.
  13406. * @param {HTMLElement} element
  13407. * @param {Object} [options={}]
  13408. * @returns {Hammer.Instance}
  13409. * @constructor
  13410. */
  13411. Hammer.Instance = function(element, options) {
  13412. var self = this;
  13413. // setup HammerJS window events and register all gestures
  13414. // this also sets up the default options
  13415. setup();
  13416. this.element = element;
  13417. // start/stop detection option
  13418. this.enabled = true;
  13419. // merge options
  13420. this.options = Hammer.utils.extend(
  13421. Hammer.utils.extend({}, Hammer.defaults),
  13422. options || {});
  13423. // add some css to the element to prevent the browser from doing its native behavoir
  13424. if(this.options.stop_browser_behavior) {
  13425. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  13426. }
  13427. // start detection on touchstart
  13428. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  13429. if(self.enabled) {
  13430. Hammer.detection.startDetect(self, ev);
  13431. }
  13432. });
  13433. // return instance
  13434. return this;
  13435. };
  13436. Hammer.Instance.prototype = {
  13437. /**
  13438. * bind events to the instance
  13439. * @param {String} gesture
  13440. * @param {Function} handler
  13441. * @returns {Hammer.Instance}
  13442. */
  13443. on: function onEvent(gesture, handler){
  13444. var gestures = gesture.split(' ');
  13445. for(var t=0; t<gestures.length; t++) {
  13446. this.element.addEventListener(gestures[t], handler, false);
  13447. }
  13448. return this;
  13449. },
  13450. /**
  13451. * unbind events to the instance
  13452. * @param {String} gesture
  13453. * @param {Function} handler
  13454. * @returns {Hammer.Instance}
  13455. */
  13456. off: function offEvent(gesture, handler){
  13457. var gestures = gesture.split(' ');
  13458. for(var t=0; t<gestures.length; t++) {
  13459. this.element.removeEventListener(gestures[t], handler, false);
  13460. }
  13461. return this;
  13462. },
  13463. /**
  13464. * trigger gesture event
  13465. * @param {String} gesture
  13466. * @param {Object} eventData
  13467. * @returns {Hammer.Instance}
  13468. */
  13469. trigger: function triggerEvent(gesture, eventData){
  13470. // create DOM event
  13471. var event = Hammer.DOCUMENT.createEvent('Event');
  13472. event.initEvent(gesture, true, true);
  13473. event.gesture = eventData;
  13474. // trigger on the target if it is in the instance element,
  13475. // this is for event delegation tricks
  13476. var element = this.element;
  13477. if(Hammer.utils.hasParent(eventData.target, element)) {
  13478. element = eventData.target;
  13479. }
  13480. element.dispatchEvent(event);
  13481. return this;
  13482. },
  13483. /**
  13484. * enable of disable hammer.js detection
  13485. * @param {Boolean} state
  13486. * @returns {Hammer.Instance}
  13487. */
  13488. enable: function enable(state) {
  13489. this.enabled = state;
  13490. return this;
  13491. }
  13492. };
  13493. /**
  13494. * this holds the last move event,
  13495. * used to fix empty touchend issue
  13496. * see the onTouch event for an explanation
  13497. * @type {Object}
  13498. */
  13499. var last_move_event = null;
  13500. /**
  13501. * when the mouse is hold down, this is true
  13502. * @type {Boolean}
  13503. */
  13504. var enable_detect = false;
  13505. /**
  13506. * when touch events have been fired, this is true
  13507. * @type {Boolean}
  13508. */
  13509. var touch_triggered = false;
  13510. Hammer.event = {
  13511. /**
  13512. * simple addEventListener
  13513. * @param {HTMLElement} element
  13514. * @param {String} type
  13515. * @param {Function} handler
  13516. */
  13517. bindDom: function(element, type, handler) {
  13518. var types = type.split(' ');
  13519. for(var t=0; t<types.length; t++) {
  13520. element.addEventListener(types[t], handler, false);
  13521. }
  13522. },
  13523. /**
  13524. * touch events with mouse fallback
  13525. * @param {HTMLElement} element
  13526. * @param {String} eventType like Hammer.EVENT_MOVE
  13527. * @param {Function} handler
  13528. */
  13529. onTouch: function onTouch(element, eventType, handler) {
  13530. var self = this;
  13531. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  13532. var sourceEventType = ev.type.toLowerCase();
  13533. // onmouseup, but when touchend has been fired we do nothing.
  13534. // this is for touchdevices which also fire a mouseup on touchend
  13535. if(sourceEventType.match(/mouse/) && touch_triggered) {
  13536. return;
  13537. }
  13538. // mousebutton must be down or a touch event
  13539. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  13540. sourceEventType.match(/pointerdown/) || // pointerevents touch
  13541. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  13542. ){
  13543. enable_detect = true;
  13544. }
  13545. // we are in a touch event, set the touch triggered bool to true,
  13546. // this for the conflicts that may occur on ios and android
  13547. if(sourceEventType.match(/touch|pointer/)) {
  13548. touch_triggered = true;
  13549. }
  13550. // count the total touches on the screen
  13551. var count_touches = 0;
  13552. // when touch has been triggered in this detection session
  13553. // and we are now handling a mouse event, we stop that to prevent conflicts
  13554. if(enable_detect) {
  13555. // update pointerevent
  13556. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  13557. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  13558. }
  13559. // touch
  13560. else if(sourceEventType.match(/touch/)) {
  13561. count_touches = ev.touches.length;
  13562. }
  13563. // mouse
  13564. else if(!touch_triggered) {
  13565. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  13566. }
  13567. // if we are in a end event, but when we remove one touch and
  13568. // we still have enough, set eventType to move
  13569. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  13570. eventType = Hammer.EVENT_MOVE;
  13571. }
  13572. // no touches, force the end event
  13573. else if(!count_touches) {
  13574. eventType = Hammer.EVENT_END;
  13575. }
  13576. // because touchend has no touches, and we often want to use these in our gestures,
  13577. // we send the last move event as our eventData in touchend
  13578. if(!count_touches && last_move_event !== null) {
  13579. ev = last_move_event;
  13580. }
  13581. // store the last move event
  13582. else {
  13583. last_move_event = ev;
  13584. }
  13585. // trigger the handler
  13586. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  13587. // remove pointerevent from list
  13588. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  13589. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  13590. }
  13591. }
  13592. //debug(sourceEventType +" "+ eventType);
  13593. // on the end we reset everything
  13594. if(!count_touches) {
  13595. last_move_event = null;
  13596. enable_detect = false;
  13597. touch_triggered = false;
  13598. Hammer.PointerEvent.reset();
  13599. }
  13600. });
  13601. },
  13602. /**
  13603. * we have different events for each device/browser
  13604. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  13605. */
  13606. determineEventTypes: function determineEventTypes() {
  13607. // determine the eventtype we want to set
  13608. var types;
  13609. // pointerEvents magic
  13610. if(Hammer.HAS_POINTEREVENTS) {
  13611. types = Hammer.PointerEvent.getEvents();
  13612. }
  13613. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  13614. else if(Hammer.NO_MOUSEEVENTS) {
  13615. types = [
  13616. 'touchstart',
  13617. 'touchmove',
  13618. 'touchend touchcancel'];
  13619. }
  13620. // for non pointer events browsers and mixed browsers,
  13621. // like chrome on windows8 touch laptop
  13622. else {
  13623. types = [
  13624. 'touchstart mousedown',
  13625. 'touchmove mousemove',
  13626. 'touchend touchcancel mouseup'];
  13627. }
  13628. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  13629. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  13630. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  13631. },
  13632. /**
  13633. * create touchlist depending on the event
  13634. * @param {Object} ev
  13635. * @param {String} eventType used by the fakemultitouch plugin
  13636. */
  13637. getTouchList: function getTouchList(ev/*, eventType*/) {
  13638. // get the fake pointerEvent touchlist
  13639. if(Hammer.HAS_POINTEREVENTS) {
  13640. return Hammer.PointerEvent.getTouchList();
  13641. }
  13642. // get the touchlist
  13643. else if(ev.touches) {
  13644. return ev.touches;
  13645. }
  13646. // make fake touchlist from mouse position
  13647. else {
  13648. return [{
  13649. identifier: 1,
  13650. pageX: ev.pageX,
  13651. pageY: ev.pageY,
  13652. target: ev.target
  13653. }];
  13654. }
  13655. },
  13656. /**
  13657. * collect event data for Hammer js
  13658. * @param {HTMLElement} element
  13659. * @param {String} eventType like Hammer.EVENT_MOVE
  13660. * @param {Object} eventData
  13661. */
  13662. collectEventData: function collectEventData(element, eventType, ev) {
  13663. var touches = this.getTouchList(ev, eventType);
  13664. // find out pointerType
  13665. var pointerType = Hammer.POINTER_TOUCH;
  13666. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  13667. pointerType = Hammer.POINTER_MOUSE;
  13668. }
  13669. return {
  13670. center : Hammer.utils.getCenter(touches),
  13671. timeStamp : new Date().getTime(),
  13672. target : ev.target,
  13673. touches : touches,
  13674. eventType : eventType,
  13675. pointerType : pointerType,
  13676. srcEvent : ev,
  13677. /**
  13678. * prevent the browser default actions
  13679. * mostly used to disable scrolling of the browser
  13680. */
  13681. preventDefault: function() {
  13682. if(this.srcEvent.preventManipulation) {
  13683. this.srcEvent.preventManipulation();
  13684. }
  13685. if(this.srcEvent.preventDefault) {
  13686. this.srcEvent.preventDefault();
  13687. }
  13688. },
  13689. /**
  13690. * stop bubbling the event up to its parents
  13691. */
  13692. stopPropagation: function() {
  13693. this.srcEvent.stopPropagation();
  13694. },
  13695. /**
  13696. * immediately stop gesture detection
  13697. * might be useful after a swipe was detected
  13698. * @return {*}
  13699. */
  13700. stopDetect: function() {
  13701. return Hammer.detection.stopDetect();
  13702. }
  13703. };
  13704. }
  13705. };
  13706. Hammer.PointerEvent = {
  13707. /**
  13708. * holds all pointers
  13709. * @type {Object}
  13710. */
  13711. pointers: {},
  13712. /**
  13713. * get a list of pointers
  13714. * @returns {Array} touchlist
  13715. */
  13716. getTouchList: function() {
  13717. var self = this;
  13718. var touchlist = [];
  13719. // we can use forEach since pointerEvents only is in IE10
  13720. Object.keys(self.pointers).sort().forEach(function(id) {
  13721. touchlist.push(self.pointers[id]);
  13722. });
  13723. return touchlist;
  13724. },
  13725. /**
  13726. * update the position of a pointer
  13727. * @param {String} type Hammer.EVENT_END
  13728. * @param {Object} pointerEvent
  13729. */
  13730. updatePointer: function(type, pointerEvent) {
  13731. if(type == Hammer.EVENT_END) {
  13732. this.pointers = {};
  13733. }
  13734. else {
  13735. pointerEvent.identifier = pointerEvent.pointerId;
  13736. this.pointers[pointerEvent.pointerId] = pointerEvent;
  13737. }
  13738. return Object.keys(this.pointers).length;
  13739. },
  13740. /**
  13741. * check if ev matches pointertype
  13742. * @param {String} pointerType Hammer.POINTER_MOUSE
  13743. * @param {PointerEvent} ev
  13744. */
  13745. matchType: function(pointerType, ev) {
  13746. if(!ev.pointerType) {
  13747. return false;
  13748. }
  13749. var types = {};
  13750. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  13751. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  13752. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  13753. return types[pointerType];
  13754. },
  13755. /**
  13756. * get events
  13757. */
  13758. getEvents: function() {
  13759. return [
  13760. 'pointerdown MSPointerDown',
  13761. 'pointermove MSPointerMove',
  13762. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  13763. ];
  13764. },
  13765. /**
  13766. * reset the list
  13767. */
  13768. reset: function() {
  13769. this.pointers = {};
  13770. }
  13771. };
  13772. Hammer.utils = {
  13773. /**
  13774. * extend method,
  13775. * also used for cloning when dest is an empty object
  13776. * @param {Object} dest
  13777. * @param {Object} src
  13778. * @parm {Boolean} merge do a merge
  13779. * @returns {Object} dest
  13780. */
  13781. extend: function extend(dest, src, merge) {
  13782. for (var key in src) {
  13783. if(dest[key] !== undefined && merge) {
  13784. continue;
  13785. }
  13786. dest[key] = src[key];
  13787. }
  13788. return dest;
  13789. },
  13790. /**
  13791. * find if a node is in the given parent
  13792. * used for event delegation tricks
  13793. * @param {HTMLElement} node
  13794. * @param {HTMLElement} parent
  13795. * @returns {boolean} has_parent
  13796. */
  13797. hasParent: function(node, parent) {
  13798. while(node){
  13799. if(node == parent) {
  13800. return true;
  13801. }
  13802. node = node.parentNode;
  13803. }
  13804. return false;
  13805. },
  13806. /**
  13807. * get the center of all the touches
  13808. * @param {Array} touches
  13809. * @returns {Object} center
  13810. */
  13811. getCenter: function getCenter(touches) {
  13812. var valuesX = [], valuesY = [];
  13813. for(var t= 0,len=touches.length; t<len; t++) {
  13814. valuesX.push(touches[t].pageX);
  13815. valuesY.push(touches[t].pageY);
  13816. }
  13817. return {
  13818. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  13819. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  13820. };
  13821. },
  13822. /**
  13823. * calculate the velocity between two points
  13824. * @param {Number} delta_time
  13825. * @param {Number} delta_x
  13826. * @param {Number} delta_y
  13827. * @returns {Object} velocity
  13828. */
  13829. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  13830. return {
  13831. x: Math.abs(delta_x / delta_time) || 0,
  13832. y: Math.abs(delta_y / delta_time) || 0
  13833. };
  13834. },
  13835. /**
  13836. * calculate the angle between two coordinates
  13837. * @param {Touch} touch1
  13838. * @param {Touch} touch2
  13839. * @returns {Number} angle
  13840. */
  13841. getAngle: function getAngle(touch1, touch2) {
  13842. var y = touch2.pageY - touch1.pageY,
  13843. x = touch2.pageX - touch1.pageX;
  13844. return Math.atan2(y, x) * 180 / Math.PI;
  13845. },
  13846. /**
  13847. * angle to direction define
  13848. * @param {Touch} touch1
  13849. * @param {Touch} touch2
  13850. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  13851. */
  13852. getDirection: function getDirection(touch1, touch2) {
  13853. var x = Math.abs(touch1.pageX - touch2.pageX),
  13854. y = Math.abs(touch1.pageY - touch2.pageY);
  13855. if(x >= y) {
  13856. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  13857. }
  13858. else {
  13859. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  13860. }
  13861. },
  13862. /**
  13863. * calculate the distance between two touches
  13864. * @param {Touch} touch1
  13865. * @param {Touch} touch2
  13866. * @returns {Number} distance
  13867. */
  13868. getDistance: function getDistance(touch1, touch2) {
  13869. var x = touch2.pageX - touch1.pageX,
  13870. y = touch2.pageY - touch1.pageY;
  13871. return Math.sqrt((x*x) + (y*y));
  13872. },
  13873. /**
  13874. * calculate the scale factor between two touchLists (fingers)
  13875. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  13876. * @param {Array} start
  13877. * @param {Array} end
  13878. * @returns {Number} scale
  13879. */
  13880. getScale: function getScale(start, end) {
  13881. // need two fingers...
  13882. if(start.length >= 2 && end.length >= 2) {
  13883. return this.getDistance(end[0], end[1]) /
  13884. this.getDistance(start[0], start[1]);
  13885. }
  13886. return 1;
  13887. },
  13888. /**
  13889. * calculate the rotation degrees between two touchLists (fingers)
  13890. * @param {Array} start
  13891. * @param {Array} end
  13892. * @returns {Number} rotation
  13893. */
  13894. getRotation: function getRotation(start, end) {
  13895. // need two fingers
  13896. if(start.length >= 2 && end.length >= 2) {
  13897. return this.getAngle(end[1], end[0]) -
  13898. this.getAngle(start[1], start[0]);
  13899. }
  13900. return 0;
  13901. },
  13902. /**
  13903. * boolean if the direction is vertical
  13904. * @param {String} direction
  13905. * @returns {Boolean} is_vertical
  13906. */
  13907. isVertical: function isVertical(direction) {
  13908. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  13909. },
  13910. /**
  13911. * stop browser default behavior with css props
  13912. * @param {HtmlElement} element
  13913. * @param {Object} css_props
  13914. */
  13915. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  13916. var prop,
  13917. vendors = ['webkit','khtml','moz','ms','o',''];
  13918. if(!css_props || !element.style) {
  13919. return;
  13920. }
  13921. // with css properties for modern browsers
  13922. for(var i = 0; i < vendors.length; i++) {
  13923. for(var p in css_props) {
  13924. if(css_props.hasOwnProperty(p)) {
  13925. prop = p;
  13926. // vender prefix at the property
  13927. if(vendors[i]) {
  13928. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  13929. }
  13930. // set the style
  13931. element.style[prop] = css_props[p];
  13932. }
  13933. }
  13934. }
  13935. // also the disable onselectstart
  13936. if(css_props.userSelect == 'none') {
  13937. element.onselectstart = function() {
  13938. return false;
  13939. };
  13940. }
  13941. }
  13942. };
  13943. Hammer.detection = {
  13944. // contains all registred Hammer.gestures in the correct order
  13945. gestures: [],
  13946. // data of the current Hammer.gesture detection session
  13947. current: null,
  13948. // the previous Hammer.gesture session data
  13949. // is a full clone of the previous gesture.current object
  13950. previous: null,
  13951. // when this becomes true, no gestures are fired
  13952. stopped: false,
  13953. /**
  13954. * start Hammer.gesture detection
  13955. * @param {Hammer.Instance} inst
  13956. * @param {Object} eventData
  13957. */
  13958. startDetect: function startDetect(inst, eventData) {
  13959. // already busy with a Hammer.gesture detection on an element
  13960. if(this.current) {
  13961. return;
  13962. }
  13963. this.stopped = false;
  13964. this.current = {
  13965. inst : inst, // reference to HammerInstance we're working for
  13966. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  13967. lastEvent : false, // last eventData
  13968. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  13969. };
  13970. this.detect(eventData);
  13971. },
  13972. /**
  13973. * Hammer.gesture detection
  13974. * @param {Object} eventData
  13975. * @param {Object} eventData
  13976. */
  13977. detect: function detect(eventData) {
  13978. if(!this.current || this.stopped) {
  13979. return;
  13980. }
  13981. // extend event data with calculations about scale, distance etc
  13982. eventData = this.extendEventData(eventData);
  13983. // instance options
  13984. var inst_options = this.current.inst.options;
  13985. // call Hammer.gesture handlers
  13986. for(var g=0,len=this.gestures.length; g<len; g++) {
  13987. var gesture = this.gestures[g];
  13988. // only when the instance options have enabled this gesture
  13989. if(!this.stopped && inst_options[gesture.name] !== false) {
  13990. // if a handler returns false, we stop with the detection
  13991. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  13992. this.stopDetect();
  13993. break;
  13994. }
  13995. }
  13996. }
  13997. // store as previous event event
  13998. if(this.current) {
  13999. this.current.lastEvent = eventData;
  14000. }
  14001. // endevent, but not the last touch, so dont stop
  14002. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  14003. this.stopDetect();
  14004. }
  14005. return eventData;
  14006. },
  14007. /**
  14008. * clear the Hammer.gesture vars
  14009. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  14010. * to stop other Hammer.gestures from being fired
  14011. */
  14012. stopDetect: function stopDetect() {
  14013. // clone current data to the store as the previous gesture
  14014. // used for the double tap gesture, since this is an other gesture detect session
  14015. this.previous = Hammer.utils.extend({}, this.current);
  14016. // reset the current
  14017. this.current = null;
  14018. // stopped!
  14019. this.stopped = true;
  14020. },
  14021. /**
  14022. * extend eventData for Hammer.gestures
  14023. * @param {Object} ev
  14024. * @returns {Object} ev
  14025. */
  14026. extendEventData: function extendEventData(ev) {
  14027. var startEv = this.current.startEvent;
  14028. // if the touches change, set the new touches over the startEvent touches
  14029. // this because touchevents don't have all the touches on touchstart, or the
  14030. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  14031. // but, sometimes it happens that both fingers are touching at the EXACT same time
  14032. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  14033. // extend 1 level deep to get the touchlist with the touch objects
  14034. startEv.touches = [];
  14035. for(var i=0,len=ev.touches.length; i<len; i++) {
  14036. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  14037. }
  14038. }
  14039. var delta_time = ev.timeStamp - startEv.timeStamp,
  14040. delta_x = ev.center.pageX - startEv.center.pageX,
  14041. delta_y = ev.center.pageY - startEv.center.pageY,
  14042. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  14043. Hammer.utils.extend(ev, {
  14044. deltaTime : delta_time,
  14045. deltaX : delta_x,
  14046. deltaY : delta_y,
  14047. velocityX : velocity.x,
  14048. velocityY : velocity.y,
  14049. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  14050. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  14051. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  14052. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  14053. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  14054. startEvent : startEv
  14055. });
  14056. return ev;
  14057. },
  14058. /**
  14059. * register new gesture
  14060. * @param {Object} gesture object, see gestures.js for documentation
  14061. * @returns {Array} gestures
  14062. */
  14063. register: function register(gesture) {
  14064. // add an enable gesture options if there is no given
  14065. var options = gesture.defaults || {};
  14066. if(options[gesture.name] === undefined) {
  14067. options[gesture.name] = true;
  14068. }
  14069. // extend Hammer default options with the Hammer.gesture options
  14070. Hammer.utils.extend(Hammer.defaults, options, true);
  14071. // set its index
  14072. gesture.index = gesture.index || 1000;
  14073. // add Hammer.gesture to the list
  14074. this.gestures.push(gesture);
  14075. // sort the list by index
  14076. this.gestures.sort(function(a, b) {
  14077. if (a.index < b.index) {
  14078. return -1;
  14079. }
  14080. if (a.index > b.index) {
  14081. return 1;
  14082. }
  14083. return 0;
  14084. });
  14085. return this.gestures;
  14086. }
  14087. };
  14088. Hammer.gestures = Hammer.gestures || {};
  14089. /**
  14090. * Custom gestures
  14091. * ==============================
  14092. *
  14093. * Gesture object
  14094. * --------------------
  14095. * The object structure of a gesture:
  14096. *
  14097. * { name: 'mygesture',
  14098. * index: 1337,
  14099. * defaults: {
  14100. * mygesture_option: true
  14101. * }
  14102. * handler: function(type, ev, inst) {
  14103. * // trigger gesture event
  14104. * inst.trigger(this.name, ev);
  14105. * }
  14106. * }
  14107. * @param {String} name
  14108. * this should be the name of the gesture, lowercase
  14109. * it is also being used to disable/enable the gesture per instance config.
  14110. *
  14111. * @param {Number} [index=1000]
  14112. * the index of the gesture, where it is going to be in the stack of gestures detection
  14113. * like when you build an gesture that depends on the drag gesture, it is a good
  14114. * idea to place it after the index of the drag gesture.
  14115. *
  14116. * @param {Object} [defaults={}]
  14117. * the default settings of the gesture. these are added to the instance settings,
  14118. * and can be overruled per instance. you can also add the name of the gesture,
  14119. * but this is also added by default (and set to true).
  14120. *
  14121. * @param {Function} handler
  14122. * this handles the gesture detection of your custom gesture and receives the
  14123. * following arguments:
  14124. *
  14125. * @param {Object} eventData
  14126. * event data containing the following properties:
  14127. * timeStamp {Number} time the event occurred
  14128. * target {HTMLElement} target element
  14129. * touches {Array} touches (fingers, pointers, mouse) on the screen
  14130. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  14131. * center {Object} center position of the touches. contains pageX and pageY
  14132. * deltaTime {Number} the total time of the touches in the screen
  14133. * deltaX {Number} the delta on x axis we haved moved
  14134. * deltaY {Number} the delta on y axis we haved moved
  14135. * velocityX {Number} the velocity on the x
  14136. * velocityY {Number} the velocity on y
  14137. * angle {Number} the angle we are moving
  14138. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  14139. * distance {Number} the distance we haved moved
  14140. * scale {Number} scaling of the touches, needs 2 touches
  14141. * rotation {Number} rotation of the touches, needs 2 touches *
  14142. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  14143. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  14144. * startEvent {Object} contains the same properties as above,
  14145. * but from the first touch. this is used to calculate
  14146. * distances, deltaTime, scaling etc
  14147. *
  14148. * @param {Hammer.Instance} inst
  14149. * the instance we are doing the detection for. you can get the options from
  14150. * the inst.options object and trigger the gesture event by calling inst.trigger
  14151. *
  14152. *
  14153. * Handle gestures
  14154. * --------------------
  14155. * inside the handler you can get/set Hammer.detection.current. This is the current
  14156. * detection session. It has the following properties
  14157. * @param {String} name
  14158. * contains the name of the gesture we have detected. it has not a real function,
  14159. * only to check in other gestures if something is detected.
  14160. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  14161. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  14162. *
  14163. * @readonly
  14164. * @param {Hammer.Instance} inst
  14165. * the instance we do the detection for
  14166. *
  14167. * @readonly
  14168. * @param {Object} startEvent
  14169. * contains the properties of the first gesture detection in this session.
  14170. * Used for calculations about timing, distance, etc.
  14171. *
  14172. * @readonly
  14173. * @param {Object} lastEvent
  14174. * contains all the properties of the last gesture detect in this session.
  14175. *
  14176. * after the gesture detection session has been completed (user has released the screen)
  14177. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  14178. * this is usefull for gestures like doubletap, where you need to know if the
  14179. * previous gesture was a tap
  14180. *
  14181. * options that have been set by the instance can be received by calling inst.options
  14182. *
  14183. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  14184. * The first param is the name of your gesture, the second the event argument
  14185. *
  14186. *
  14187. * Register gestures
  14188. * --------------------
  14189. * When an gesture is added to the Hammer.gestures object, it is auto registered
  14190. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  14191. * manually and pass your gesture object as a param
  14192. *
  14193. */
  14194. /**
  14195. * Hold
  14196. * Touch stays at the same place for x time
  14197. * @events hold
  14198. */
  14199. Hammer.gestures.Hold = {
  14200. name: 'hold',
  14201. index: 10,
  14202. defaults: {
  14203. hold_timeout : 500,
  14204. hold_threshold : 1
  14205. },
  14206. timer: null,
  14207. handler: function holdGesture(ev, inst) {
  14208. switch(ev.eventType) {
  14209. case Hammer.EVENT_START:
  14210. // clear any running timers
  14211. clearTimeout(this.timer);
  14212. // set the gesture so we can check in the timeout if it still is
  14213. Hammer.detection.current.name = this.name;
  14214. // set timer and if after the timeout it still is hold,
  14215. // we trigger the hold event
  14216. this.timer = setTimeout(function() {
  14217. if(Hammer.detection.current.name == 'hold') {
  14218. inst.trigger('hold', ev);
  14219. }
  14220. }, inst.options.hold_timeout);
  14221. break;
  14222. // when you move or end we clear the timer
  14223. case Hammer.EVENT_MOVE:
  14224. if(ev.distance > inst.options.hold_threshold) {
  14225. clearTimeout(this.timer);
  14226. }
  14227. break;
  14228. case Hammer.EVENT_END:
  14229. clearTimeout(this.timer);
  14230. break;
  14231. }
  14232. }
  14233. };
  14234. /**
  14235. * Tap/DoubleTap
  14236. * Quick touch at a place or double at the same place
  14237. * @events tap, doubletap
  14238. */
  14239. Hammer.gestures.Tap = {
  14240. name: 'tap',
  14241. index: 100,
  14242. defaults: {
  14243. tap_max_touchtime : 250,
  14244. tap_max_distance : 10,
  14245. tap_always : true,
  14246. doubletap_distance : 20,
  14247. doubletap_interval : 300
  14248. },
  14249. handler: function tapGesture(ev, inst) {
  14250. if(ev.eventType == Hammer.EVENT_END) {
  14251. // previous gesture, for the double tap since these are two different gesture detections
  14252. var prev = Hammer.detection.previous,
  14253. did_doubletap = false;
  14254. // when the touchtime is higher then the max touch time
  14255. // or when the moving distance is too much
  14256. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  14257. ev.distance > inst.options.tap_max_distance) {
  14258. return;
  14259. }
  14260. // check if double tap
  14261. if(prev && prev.name == 'tap' &&
  14262. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  14263. ev.distance < inst.options.doubletap_distance) {
  14264. inst.trigger('doubletap', ev);
  14265. did_doubletap = true;
  14266. }
  14267. // do a single tap
  14268. if(!did_doubletap || inst.options.tap_always) {
  14269. Hammer.detection.current.name = 'tap';
  14270. inst.trigger(Hammer.detection.current.name, ev);
  14271. }
  14272. }
  14273. }
  14274. };
  14275. /**
  14276. * Swipe
  14277. * triggers swipe events when the end velocity is above the threshold
  14278. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  14279. */
  14280. Hammer.gestures.Swipe = {
  14281. name: 'swipe',
  14282. index: 40,
  14283. defaults: {
  14284. // set 0 for unlimited, but this can conflict with transform
  14285. swipe_max_touches : 1,
  14286. swipe_velocity : 0.7
  14287. },
  14288. handler: function swipeGesture(ev, inst) {
  14289. if(ev.eventType == Hammer.EVENT_END) {
  14290. // max touches
  14291. if(inst.options.swipe_max_touches > 0 &&
  14292. ev.touches.length > inst.options.swipe_max_touches) {
  14293. return;
  14294. }
  14295. // when the distance we moved is too small we skip this gesture
  14296. // or we can be already in dragging
  14297. if(ev.velocityX > inst.options.swipe_velocity ||
  14298. ev.velocityY > inst.options.swipe_velocity) {
  14299. // trigger swipe events
  14300. inst.trigger(this.name, ev);
  14301. inst.trigger(this.name + ev.direction, ev);
  14302. }
  14303. }
  14304. }
  14305. };
  14306. /**
  14307. * Drag
  14308. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  14309. * moving left and right is a good practice. When all the drag events are blocking
  14310. * you disable scrolling on that area.
  14311. * @events drag, drapleft, dragright, dragup, dragdown
  14312. */
  14313. Hammer.gestures.Drag = {
  14314. name: 'drag',
  14315. index: 50,
  14316. defaults: {
  14317. drag_min_distance : 10,
  14318. // set 0 for unlimited, but this can conflict with transform
  14319. drag_max_touches : 1,
  14320. // prevent default browser behavior when dragging occurs
  14321. // be careful with it, it makes the element a blocking element
  14322. // when you are using the drag gesture, it is a good practice to set this true
  14323. drag_block_horizontal : false,
  14324. drag_block_vertical : false,
  14325. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  14326. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  14327. drag_lock_to_axis : false,
  14328. // drag lock only kicks in when distance > drag_lock_min_distance
  14329. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  14330. drag_lock_min_distance : 25
  14331. },
  14332. triggered: false,
  14333. handler: function dragGesture(ev, inst) {
  14334. // current gesture isnt drag, but dragged is true
  14335. // this means an other gesture is busy. now call dragend
  14336. if(Hammer.detection.current.name != this.name && this.triggered) {
  14337. inst.trigger(this.name +'end', ev);
  14338. this.triggered = false;
  14339. return;
  14340. }
  14341. // max touches
  14342. if(inst.options.drag_max_touches > 0 &&
  14343. ev.touches.length > inst.options.drag_max_touches) {
  14344. return;
  14345. }
  14346. switch(ev.eventType) {
  14347. case Hammer.EVENT_START:
  14348. this.triggered = false;
  14349. break;
  14350. case Hammer.EVENT_MOVE:
  14351. // when the distance we moved is too small we skip this gesture
  14352. // or we can be already in dragging
  14353. if(ev.distance < inst.options.drag_min_distance &&
  14354. Hammer.detection.current.name != this.name) {
  14355. return;
  14356. }
  14357. // we are dragging!
  14358. Hammer.detection.current.name = this.name;
  14359. // lock drag to axis?
  14360. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  14361. ev.drag_locked_to_axis = true;
  14362. }
  14363. var last_direction = Hammer.detection.current.lastEvent.direction;
  14364. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  14365. // keep direction on the axis that the drag gesture started on
  14366. if(Hammer.utils.isVertical(last_direction)) {
  14367. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  14368. }
  14369. else {
  14370. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  14371. }
  14372. }
  14373. // first time, trigger dragstart event
  14374. if(!this.triggered) {
  14375. inst.trigger(this.name +'start', ev);
  14376. this.triggered = true;
  14377. }
  14378. // trigger normal event
  14379. inst.trigger(this.name, ev);
  14380. // direction event, like dragdown
  14381. inst.trigger(this.name + ev.direction, ev);
  14382. // block the browser events
  14383. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  14384. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  14385. ev.preventDefault();
  14386. }
  14387. break;
  14388. case Hammer.EVENT_END:
  14389. // trigger dragend
  14390. if(this.triggered) {
  14391. inst.trigger(this.name +'end', ev);
  14392. }
  14393. this.triggered = false;
  14394. break;
  14395. }
  14396. }
  14397. };
  14398. /**
  14399. * Transform
  14400. * User want to scale or rotate with 2 fingers
  14401. * @events transform, pinch, pinchin, pinchout, rotate
  14402. */
  14403. Hammer.gestures.Transform = {
  14404. name: 'transform',
  14405. index: 45,
  14406. defaults: {
  14407. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  14408. transform_min_scale : 0.01,
  14409. // rotation in degrees
  14410. transform_min_rotation : 1,
  14411. // prevent default browser behavior when two touches are on the screen
  14412. // but it makes the element a blocking element
  14413. // when you are using the transform gesture, it is a good practice to set this true
  14414. transform_always_block : false
  14415. },
  14416. triggered: false,
  14417. handler: function transformGesture(ev, inst) {
  14418. // current gesture isnt drag, but dragged is true
  14419. // this means an other gesture is busy. now call dragend
  14420. if(Hammer.detection.current.name != this.name && this.triggered) {
  14421. inst.trigger(this.name +'end', ev);
  14422. this.triggered = false;
  14423. return;
  14424. }
  14425. // atleast multitouch
  14426. if(ev.touches.length < 2) {
  14427. return;
  14428. }
  14429. // prevent default when two fingers are on the screen
  14430. if(inst.options.transform_always_block) {
  14431. ev.preventDefault();
  14432. }
  14433. switch(ev.eventType) {
  14434. case Hammer.EVENT_START:
  14435. this.triggered = false;
  14436. break;
  14437. case Hammer.EVENT_MOVE:
  14438. var scale_threshold = Math.abs(1-ev.scale);
  14439. var rotation_threshold = Math.abs(ev.rotation);
  14440. // when the distance we moved is too small we skip this gesture
  14441. // or we can be already in dragging
  14442. if(scale_threshold < inst.options.transform_min_scale &&
  14443. rotation_threshold < inst.options.transform_min_rotation) {
  14444. return;
  14445. }
  14446. // we are transforming!
  14447. Hammer.detection.current.name = this.name;
  14448. // first time, trigger dragstart event
  14449. if(!this.triggered) {
  14450. inst.trigger(this.name +'start', ev);
  14451. this.triggered = true;
  14452. }
  14453. inst.trigger(this.name, ev); // basic transform event
  14454. // trigger rotate event
  14455. if(rotation_threshold > inst.options.transform_min_rotation) {
  14456. inst.trigger('rotate', ev);
  14457. }
  14458. // trigger pinch event
  14459. if(scale_threshold > inst.options.transform_min_scale) {
  14460. inst.trigger('pinch', ev);
  14461. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  14462. }
  14463. break;
  14464. case Hammer.EVENT_END:
  14465. // trigger dragend
  14466. if(this.triggered) {
  14467. inst.trigger(this.name +'end', ev);
  14468. }
  14469. this.triggered = false;
  14470. break;
  14471. }
  14472. }
  14473. };
  14474. /**
  14475. * Touch
  14476. * Called as first, tells the user has touched the screen
  14477. * @events touch
  14478. */
  14479. Hammer.gestures.Touch = {
  14480. name: 'touch',
  14481. index: -Infinity,
  14482. defaults: {
  14483. // call preventDefault at touchstart, and makes the element blocking by
  14484. // disabling the scrolling of the page, but it improves gestures like
  14485. // transforming and dragging.
  14486. // be careful with using this, it can be very annoying for users to be stuck
  14487. // on the page
  14488. prevent_default: false,
  14489. // disable mouse events, so only touch (or pen!) input triggers events
  14490. prevent_mouseevents: false
  14491. },
  14492. handler: function touchGesture(ev, inst) {
  14493. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  14494. ev.stopDetect();
  14495. return;
  14496. }
  14497. if(inst.options.prevent_default) {
  14498. ev.preventDefault();
  14499. }
  14500. if(ev.eventType == Hammer.EVENT_START) {
  14501. inst.trigger(this.name, ev);
  14502. }
  14503. }
  14504. };
  14505. /**
  14506. * Release
  14507. * Called as last, tells the user has released the screen
  14508. * @events release
  14509. */
  14510. Hammer.gestures.Release = {
  14511. name: 'release',
  14512. index: Infinity,
  14513. handler: function releaseGesture(ev, inst) {
  14514. if(ev.eventType == Hammer.EVENT_END) {
  14515. inst.trigger(this.name, ev);
  14516. }
  14517. }
  14518. };
  14519. // node export
  14520. if(typeof module === 'object' && typeof module.exports === 'object'){
  14521. module.exports = Hammer;
  14522. }
  14523. // just window export
  14524. else {
  14525. window.Hammer = Hammer;
  14526. // requireJS module definition
  14527. if(typeof window.define === 'function' && window.define.amd) {
  14528. window.define('hammer', [], function() {
  14529. return Hammer;
  14530. });
  14531. }
  14532. }
  14533. })(this);
  14534. },{}],3:[function(require,module,exports){
  14535. //! moment.js
  14536. //! version : 2.5.1
  14537. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  14538. //! license : MIT
  14539. //! momentjs.com
  14540. (function (undefined) {
  14541. /************************************
  14542. Constants
  14543. ************************************/
  14544. var moment,
  14545. VERSION = "2.5.1",
  14546. global = this,
  14547. round = Math.round,
  14548. i,
  14549. YEAR = 0,
  14550. MONTH = 1,
  14551. DATE = 2,
  14552. HOUR = 3,
  14553. MINUTE = 4,
  14554. SECOND = 5,
  14555. MILLISECOND = 6,
  14556. // internal storage for language config files
  14557. languages = {},
  14558. // moment internal properties
  14559. momentProperties = {
  14560. _isAMomentObject: null,
  14561. _i : null,
  14562. _f : null,
  14563. _l : null,
  14564. _strict : null,
  14565. _isUTC : null,
  14566. _offset : null, // optional. Combine with _isUTC
  14567. _pf : null,
  14568. _lang : null // optional
  14569. },
  14570. // check for nodeJS
  14571. hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
  14572. // ASP.NET json date format regex
  14573. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  14574. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  14575. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  14576. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  14577. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  14578. // format tokens
  14579. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,
  14580. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  14581. // parsing token regexes
  14582. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  14583. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  14584. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  14585. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  14586. parseTokenDigits = /\d+/, // nonzero number of digits
  14587. 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.
  14588. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  14589. parseTokenT = /T/i, // T (ISO separator)
  14590. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  14591. //strict parsing regexes
  14592. parseTokenOneDigit = /\d/, // 0 - 9
  14593. parseTokenTwoDigits = /\d\d/, // 00 - 99
  14594. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  14595. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  14596. parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999
  14597. parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf
  14598. // iso 8601 regex
  14599. // 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)
  14600. 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)?)?$/,
  14601. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  14602. isoDates = [
  14603. ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/],
  14604. ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/],
  14605. ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/],
  14606. ['GGGG-[W]WW', /\d{4}-W\d{2}/],
  14607. ['YYYY-DDD', /\d{4}-\d{3}/]
  14608. ],
  14609. // iso time formats and regexes
  14610. isoTimes = [
  14611. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  14612. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  14613. ['HH:mm', /(T| )\d\d:\d\d/],
  14614. ['HH', /(T| )\d\d/]
  14615. ],
  14616. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  14617. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  14618. // getter and setter names
  14619. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  14620. unitMillisecondFactors = {
  14621. 'Milliseconds' : 1,
  14622. 'Seconds' : 1e3,
  14623. 'Minutes' : 6e4,
  14624. 'Hours' : 36e5,
  14625. 'Days' : 864e5,
  14626. 'Months' : 2592e6,
  14627. 'Years' : 31536e6
  14628. },
  14629. unitAliases = {
  14630. ms : 'millisecond',
  14631. s : 'second',
  14632. m : 'minute',
  14633. h : 'hour',
  14634. d : 'day',
  14635. D : 'date',
  14636. w : 'week',
  14637. W : 'isoWeek',
  14638. M : 'month',
  14639. y : 'year',
  14640. DDD : 'dayOfYear',
  14641. e : 'weekday',
  14642. E : 'isoWeekday',
  14643. gg: 'weekYear',
  14644. GG: 'isoWeekYear'
  14645. },
  14646. camelFunctions = {
  14647. dayofyear : 'dayOfYear',
  14648. isoweekday : 'isoWeekday',
  14649. isoweek : 'isoWeek',
  14650. weekyear : 'weekYear',
  14651. isoweekyear : 'isoWeekYear'
  14652. },
  14653. // format function strings
  14654. formatFunctions = {},
  14655. // tokens to ordinalize and pad
  14656. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  14657. paddedTokens = 'M D H h m s w W'.split(' '),
  14658. formatTokenFunctions = {
  14659. M : function () {
  14660. return this.month() + 1;
  14661. },
  14662. MMM : function (format) {
  14663. return this.lang().monthsShort(this, format);
  14664. },
  14665. MMMM : function (format) {
  14666. return this.lang().months(this, format);
  14667. },
  14668. D : function () {
  14669. return this.date();
  14670. },
  14671. DDD : function () {
  14672. return this.dayOfYear();
  14673. },
  14674. d : function () {
  14675. return this.day();
  14676. },
  14677. dd : function (format) {
  14678. return this.lang().weekdaysMin(this, format);
  14679. },
  14680. ddd : function (format) {
  14681. return this.lang().weekdaysShort(this, format);
  14682. },
  14683. dddd : function (format) {
  14684. return this.lang().weekdays(this, format);
  14685. },
  14686. w : function () {
  14687. return this.week();
  14688. },
  14689. W : function () {
  14690. return this.isoWeek();
  14691. },
  14692. YY : function () {
  14693. return leftZeroFill(this.year() % 100, 2);
  14694. },
  14695. YYYY : function () {
  14696. return leftZeroFill(this.year(), 4);
  14697. },
  14698. YYYYY : function () {
  14699. return leftZeroFill(this.year(), 5);
  14700. },
  14701. YYYYYY : function () {
  14702. var y = this.year(), sign = y >= 0 ? '+' : '-';
  14703. return sign + leftZeroFill(Math.abs(y), 6);
  14704. },
  14705. gg : function () {
  14706. return leftZeroFill(this.weekYear() % 100, 2);
  14707. },
  14708. gggg : function () {
  14709. return leftZeroFill(this.weekYear(), 4);
  14710. },
  14711. ggggg : function () {
  14712. return leftZeroFill(this.weekYear(), 5);
  14713. },
  14714. GG : function () {
  14715. return leftZeroFill(this.isoWeekYear() % 100, 2);
  14716. },
  14717. GGGG : function () {
  14718. return leftZeroFill(this.isoWeekYear(), 4);
  14719. },
  14720. GGGGG : function () {
  14721. return leftZeroFill(this.isoWeekYear(), 5);
  14722. },
  14723. e : function () {
  14724. return this.weekday();
  14725. },
  14726. E : function () {
  14727. return this.isoWeekday();
  14728. },
  14729. a : function () {
  14730. return this.lang().meridiem(this.hours(), this.minutes(), true);
  14731. },
  14732. A : function () {
  14733. return this.lang().meridiem(this.hours(), this.minutes(), false);
  14734. },
  14735. H : function () {
  14736. return this.hours();
  14737. },
  14738. h : function () {
  14739. return this.hours() % 12 || 12;
  14740. },
  14741. m : function () {
  14742. return this.minutes();
  14743. },
  14744. s : function () {
  14745. return this.seconds();
  14746. },
  14747. S : function () {
  14748. return toInt(this.milliseconds() / 100);
  14749. },
  14750. SS : function () {
  14751. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  14752. },
  14753. SSS : function () {
  14754. return leftZeroFill(this.milliseconds(), 3);
  14755. },
  14756. SSSS : function () {
  14757. return leftZeroFill(this.milliseconds(), 3);
  14758. },
  14759. Z : function () {
  14760. var a = -this.zone(),
  14761. b = "+";
  14762. if (a < 0) {
  14763. a = -a;
  14764. b = "-";
  14765. }
  14766. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  14767. },
  14768. ZZ : function () {
  14769. var a = -this.zone(),
  14770. b = "+";
  14771. if (a < 0) {
  14772. a = -a;
  14773. b = "-";
  14774. }
  14775. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  14776. },
  14777. z : function () {
  14778. return this.zoneAbbr();
  14779. },
  14780. zz : function () {
  14781. return this.zoneName();
  14782. },
  14783. X : function () {
  14784. return this.unix();
  14785. },
  14786. Q : function () {
  14787. return this.quarter();
  14788. }
  14789. },
  14790. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  14791. function defaultParsingFlags() {
  14792. // We need to deep clone this object, and es5 standard is not very
  14793. // helpful.
  14794. return {
  14795. empty : false,
  14796. unusedTokens : [],
  14797. unusedInput : [],
  14798. overflow : -2,
  14799. charsLeftOver : 0,
  14800. nullInput : false,
  14801. invalidMonth : null,
  14802. invalidFormat : false,
  14803. userInvalidated : false,
  14804. iso: false
  14805. };
  14806. }
  14807. function padToken(func, count) {
  14808. return function (a) {
  14809. return leftZeroFill(func.call(this, a), count);
  14810. };
  14811. }
  14812. function ordinalizeToken(func, period) {
  14813. return function (a) {
  14814. return this.lang().ordinal(func.call(this, a), period);
  14815. };
  14816. }
  14817. while (ordinalizeTokens.length) {
  14818. i = ordinalizeTokens.pop();
  14819. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  14820. }
  14821. while (paddedTokens.length) {
  14822. i = paddedTokens.pop();
  14823. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  14824. }
  14825. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  14826. /************************************
  14827. Constructors
  14828. ************************************/
  14829. function Language() {
  14830. }
  14831. // Moment prototype object
  14832. function Moment(config) {
  14833. checkOverflow(config);
  14834. extend(this, config);
  14835. }
  14836. // Duration Constructor
  14837. function Duration(duration) {
  14838. var normalizedInput = normalizeObjectUnits(duration),
  14839. years = normalizedInput.year || 0,
  14840. months = normalizedInput.month || 0,
  14841. weeks = normalizedInput.week || 0,
  14842. days = normalizedInput.day || 0,
  14843. hours = normalizedInput.hour || 0,
  14844. minutes = normalizedInput.minute || 0,
  14845. seconds = normalizedInput.second || 0,
  14846. milliseconds = normalizedInput.millisecond || 0;
  14847. // representation for dateAddRemove
  14848. this._milliseconds = +milliseconds +
  14849. seconds * 1e3 + // 1000
  14850. minutes * 6e4 + // 1000 * 60
  14851. hours * 36e5; // 1000 * 60 * 60
  14852. // Because of dateAddRemove treats 24 hours as different from a
  14853. // day when working around DST, we need to store them separately
  14854. this._days = +days +
  14855. weeks * 7;
  14856. // It is impossible translate months into days without knowing
  14857. // which months you are are talking about, so we have to store
  14858. // it separately.
  14859. this._months = +months +
  14860. years * 12;
  14861. this._data = {};
  14862. this._bubble();
  14863. }
  14864. /************************************
  14865. Helpers
  14866. ************************************/
  14867. function extend(a, b) {
  14868. for (var i in b) {
  14869. if (b.hasOwnProperty(i)) {
  14870. a[i] = b[i];
  14871. }
  14872. }
  14873. if (b.hasOwnProperty("toString")) {
  14874. a.toString = b.toString;
  14875. }
  14876. if (b.hasOwnProperty("valueOf")) {
  14877. a.valueOf = b.valueOf;
  14878. }
  14879. return a;
  14880. }
  14881. function cloneMoment(m) {
  14882. var result = {}, i;
  14883. for (i in m) {
  14884. if (m.hasOwnProperty(i) && momentProperties.hasOwnProperty(i)) {
  14885. result[i] = m[i];
  14886. }
  14887. }
  14888. return result;
  14889. }
  14890. function absRound(number) {
  14891. if (number < 0) {
  14892. return Math.ceil(number);
  14893. } else {
  14894. return Math.floor(number);
  14895. }
  14896. }
  14897. // left zero fill a number
  14898. // see http://jsperf.com/left-zero-filling for performance comparison
  14899. function leftZeroFill(number, targetLength, forceSign) {
  14900. var output = '' + Math.abs(number),
  14901. sign = number >= 0;
  14902. while (output.length < targetLength) {
  14903. output = '0' + output;
  14904. }
  14905. return (sign ? (forceSign ? '+' : '') : '-') + output;
  14906. }
  14907. // helper function for _.addTime and _.subtractTime
  14908. function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
  14909. var milliseconds = duration._milliseconds,
  14910. days = duration._days,
  14911. months = duration._months,
  14912. minutes,
  14913. hours;
  14914. if (milliseconds) {
  14915. mom._d.setTime(+mom._d + milliseconds * isAdding);
  14916. }
  14917. // store the minutes and hours so we can restore them
  14918. if (days || months) {
  14919. minutes = mom.minute();
  14920. hours = mom.hour();
  14921. }
  14922. if (days) {
  14923. mom.date(mom.date() + days * isAdding);
  14924. }
  14925. if (months) {
  14926. mom.month(mom.month() + months * isAdding);
  14927. }
  14928. if (milliseconds && !ignoreUpdateOffset) {
  14929. moment.updateOffset(mom);
  14930. }
  14931. // restore the minutes and hours after possibly changing dst
  14932. if (days || months) {
  14933. mom.minute(minutes);
  14934. mom.hour(hours);
  14935. }
  14936. }
  14937. // check if is an array
  14938. function isArray(input) {
  14939. return Object.prototype.toString.call(input) === '[object Array]';
  14940. }
  14941. function isDate(input) {
  14942. return Object.prototype.toString.call(input) === '[object Date]' ||
  14943. input instanceof Date;
  14944. }
  14945. // compare two arrays, return the number of differences
  14946. function compareArrays(array1, array2, dontConvert) {
  14947. var len = Math.min(array1.length, array2.length),
  14948. lengthDiff = Math.abs(array1.length - array2.length),
  14949. diffs = 0,
  14950. i;
  14951. for (i = 0; i < len; i++) {
  14952. if ((dontConvert && array1[i] !== array2[i]) ||
  14953. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  14954. diffs++;
  14955. }
  14956. }
  14957. return diffs + lengthDiff;
  14958. }
  14959. function normalizeUnits(units) {
  14960. if (units) {
  14961. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  14962. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  14963. }
  14964. return units;
  14965. }
  14966. function normalizeObjectUnits(inputObject) {
  14967. var normalizedInput = {},
  14968. normalizedProp,
  14969. prop;
  14970. for (prop in inputObject) {
  14971. if (inputObject.hasOwnProperty(prop)) {
  14972. normalizedProp = normalizeUnits(prop);
  14973. if (normalizedProp) {
  14974. normalizedInput[normalizedProp] = inputObject[prop];
  14975. }
  14976. }
  14977. }
  14978. return normalizedInput;
  14979. }
  14980. function makeList(field) {
  14981. var count, setter;
  14982. if (field.indexOf('week') === 0) {
  14983. count = 7;
  14984. setter = 'day';
  14985. }
  14986. else if (field.indexOf('month') === 0) {
  14987. count = 12;
  14988. setter = 'month';
  14989. }
  14990. else {
  14991. return;
  14992. }
  14993. moment[field] = function (format, index) {
  14994. var i, getter,
  14995. method = moment.fn._lang[field],
  14996. results = [];
  14997. if (typeof format === 'number') {
  14998. index = format;
  14999. format = undefined;
  15000. }
  15001. getter = function (i) {
  15002. var m = moment().utc().set(setter, i);
  15003. return method.call(moment.fn._lang, m, format || '');
  15004. };
  15005. if (index != null) {
  15006. return getter(index);
  15007. }
  15008. else {
  15009. for (i = 0; i < count; i++) {
  15010. results.push(getter(i));
  15011. }
  15012. return results;
  15013. }
  15014. };
  15015. }
  15016. function toInt(argumentForCoercion) {
  15017. var coercedNumber = +argumentForCoercion,
  15018. value = 0;
  15019. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  15020. if (coercedNumber >= 0) {
  15021. value = Math.floor(coercedNumber);
  15022. } else {
  15023. value = Math.ceil(coercedNumber);
  15024. }
  15025. }
  15026. return value;
  15027. }
  15028. function daysInMonth(year, month) {
  15029. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  15030. }
  15031. function daysInYear(year) {
  15032. return isLeapYear(year) ? 366 : 365;
  15033. }
  15034. function isLeapYear(year) {
  15035. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  15036. }
  15037. function checkOverflow(m) {
  15038. var overflow;
  15039. if (m._a && m._pf.overflow === -2) {
  15040. overflow =
  15041. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  15042. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  15043. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  15044. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  15045. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  15046. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  15047. -1;
  15048. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  15049. overflow = DATE;
  15050. }
  15051. m._pf.overflow = overflow;
  15052. }
  15053. }
  15054. function isValid(m) {
  15055. if (m._isValid == null) {
  15056. m._isValid = !isNaN(m._d.getTime()) &&
  15057. m._pf.overflow < 0 &&
  15058. !m._pf.empty &&
  15059. !m._pf.invalidMonth &&
  15060. !m._pf.nullInput &&
  15061. !m._pf.invalidFormat &&
  15062. !m._pf.userInvalidated;
  15063. if (m._strict) {
  15064. m._isValid = m._isValid &&
  15065. m._pf.charsLeftOver === 0 &&
  15066. m._pf.unusedTokens.length === 0;
  15067. }
  15068. }
  15069. return m._isValid;
  15070. }
  15071. function normalizeLanguage(key) {
  15072. return key ? key.toLowerCase().replace('_', '-') : key;
  15073. }
  15074. // Return a moment from input, that is local/utc/zone equivalent to model.
  15075. function makeAs(input, model) {
  15076. return model._isUTC ? moment(input).zone(model._offset || 0) :
  15077. moment(input).local();
  15078. }
  15079. /************************************
  15080. Languages
  15081. ************************************/
  15082. extend(Language.prototype, {
  15083. set : function (config) {
  15084. var prop, i;
  15085. for (i in config) {
  15086. prop = config[i];
  15087. if (typeof prop === 'function') {
  15088. this[i] = prop;
  15089. } else {
  15090. this['_' + i] = prop;
  15091. }
  15092. }
  15093. },
  15094. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  15095. months : function (m) {
  15096. return this._months[m.month()];
  15097. },
  15098. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  15099. monthsShort : function (m) {
  15100. return this._monthsShort[m.month()];
  15101. },
  15102. monthsParse : function (monthName) {
  15103. var i, mom, regex;
  15104. if (!this._monthsParse) {
  15105. this._monthsParse = [];
  15106. }
  15107. for (i = 0; i < 12; i++) {
  15108. // make the regex if we don't have it already
  15109. if (!this._monthsParse[i]) {
  15110. mom = moment.utc([2000, i]);
  15111. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  15112. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  15113. }
  15114. // test the regex
  15115. if (this._monthsParse[i].test(monthName)) {
  15116. return i;
  15117. }
  15118. }
  15119. },
  15120. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  15121. weekdays : function (m) {
  15122. return this._weekdays[m.day()];
  15123. },
  15124. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  15125. weekdaysShort : function (m) {
  15126. return this._weekdaysShort[m.day()];
  15127. },
  15128. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  15129. weekdaysMin : function (m) {
  15130. return this._weekdaysMin[m.day()];
  15131. },
  15132. weekdaysParse : function (weekdayName) {
  15133. var i, mom, regex;
  15134. if (!this._weekdaysParse) {
  15135. this._weekdaysParse = [];
  15136. }
  15137. for (i = 0; i < 7; i++) {
  15138. // make the regex if we don't have it already
  15139. if (!this._weekdaysParse[i]) {
  15140. mom = moment([2000, 1]).day(i);
  15141. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  15142. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  15143. }
  15144. // test the regex
  15145. if (this._weekdaysParse[i].test(weekdayName)) {
  15146. return i;
  15147. }
  15148. }
  15149. },
  15150. _longDateFormat : {
  15151. LT : "h:mm A",
  15152. L : "MM/DD/YYYY",
  15153. LL : "MMMM D YYYY",
  15154. LLL : "MMMM D YYYY LT",
  15155. LLLL : "dddd, MMMM D YYYY LT"
  15156. },
  15157. longDateFormat : function (key) {
  15158. var output = this._longDateFormat[key];
  15159. if (!output && this._longDateFormat[key.toUpperCase()]) {
  15160. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  15161. return val.slice(1);
  15162. });
  15163. this._longDateFormat[key] = output;
  15164. }
  15165. return output;
  15166. },
  15167. isPM : function (input) {
  15168. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  15169. // Using charAt should be more compatible.
  15170. return ((input + '').toLowerCase().charAt(0) === 'p');
  15171. },
  15172. _meridiemParse : /[ap]\.?m?\.?/i,
  15173. meridiem : function (hours, minutes, isLower) {
  15174. if (hours > 11) {
  15175. return isLower ? 'pm' : 'PM';
  15176. } else {
  15177. return isLower ? 'am' : 'AM';
  15178. }
  15179. },
  15180. _calendar : {
  15181. sameDay : '[Today at] LT',
  15182. nextDay : '[Tomorrow at] LT',
  15183. nextWeek : 'dddd [at] LT',
  15184. lastDay : '[Yesterday at] LT',
  15185. lastWeek : '[Last] dddd [at] LT',
  15186. sameElse : 'L'
  15187. },
  15188. calendar : function (key, mom) {
  15189. var output = this._calendar[key];
  15190. return typeof output === 'function' ? output.apply(mom) : output;
  15191. },
  15192. _relativeTime : {
  15193. future : "in %s",
  15194. past : "%s ago",
  15195. s : "a few seconds",
  15196. m : "a minute",
  15197. mm : "%d minutes",
  15198. h : "an hour",
  15199. hh : "%d hours",
  15200. d : "a day",
  15201. dd : "%d days",
  15202. M : "a month",
  15203. MM : "%d months",
  15204. y : "a year",
  15205. yy : "%d years"
  15206. },
  15207. relativeTime : function (number, withoutSuffix, string, isFuture) {
  15208. var output = this._relativeTime[string];
  15209. return (typeof output === 'function') ?
  15210. output(number, withoutSuffix, string, isFuture) :
  15211. output.replace(/%d/i, number);
  15212. },
  15213. pastFuture : function (diff, output) {
  15214. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  15215. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  15216. },
  15217. ordinal : function (number) {
  15218. return this._ordinal.replace("%d", number);
  15219. },
  15220. _ordinal : "%d",
  15221. preparse : function (string) {
  15222. return string;
  15223. },
  15224. postformat : function (string) {
  15225. return string;
  15226. },
  15227. week : function (mom) {
  15228. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  15229. },
  15230. _week : {
  15231. dow : 0, // Sunday is the first day of the week.
  15232. doy : 6 // The week that contains Jan 1st is the first week of the year.
  15233. },
  15234. _invalidDate: 'Invalid date',
  15235. invalidDate: function () {
  15236. return this._invalidDate;
  15237. }
  15238. });
  15239. // Loads a language definition into the `languages` cache. The function
  15240. // takes a key and optionally values. If not in the browser and no values
  15241. // are provided, it will load the language file module. As a convenience,
  15242. // this function also returns the language values.
  15243. function loadLang(key, values) {
  15244. values.abbr = key;
  15245. if (!languages[key]) {
  15246. languages[key] = new Language();
  15247. }
  15248. languages[key].set(values);
  15249. return languages[key];
  15250. }
  15251. // Remove a language from the `languages` cache. Mostly useful in tests.
  15252. function unloadLang(key) {
  15253. delete languages[key];
  15254. }
  15255. // Determines which language definition to use and returns it.
  15256. //
  15257. // With no parameters, it will return the global language. If you
  15258. // pass in a language key, such as 'en', it will return the
  15259. // definition for 'en', so long as 'en' has already been loaded using
  15260. // moment.lang.
  15261. function getLangDefinition(key) {
  15262. var i = 0, j, lang, next, split,
  15263. get = function (k) {
  15264. if (!languages[k] && hasModule) {
  15265. try {
  15266. require('./lang/' + k);
  15267. } catch (e) { }
  15268. }
  15269. return languages[k];
  15270. };
  15271. if (!key) {
  15272. return moment.fn._lang;
  15273. }
  15274. if (!isArray(key)) {
  15275. //short-circuit everything else
  15276. lang = get(key);
  15277. if (lang) {
  15278. return lang;
  15279. }
  15280. key = [key];
  15281. }
  15282. //pick the language from the array
  15283. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  15284. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  15285. while (i < key.length) {
  15286. split = normalizeLanguage(key[i]).split('-');
  15287. j = split.length;
  15288. next = normalizeLanguage(key[i + 1]);
  15289. next = next ? next.split('-') : null;
  15290. while (j > 0) {
  15291. lang = get(split.slice(0, j).join('-'));
  15292. if (lang) {
  15293. return lang;
  15294. }
  15295. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  15296. //the next array item is better than a shallower substring of this one
  15297. break;
  15298. }
  15299. j--;
  15300. }
  15301. i++;
  15302. }
  15303. return moment.fn._lang;
  15304. }
  15305. /************************************
  15306. Formatting
  15307. ************************************/
  15308. function removeFormattingTokens(input) {
  15309. if (input.match(/\[[\s\S]/)) {
  15310. return input.replace(/^\[|\]$/g, "");
  15311. }
  15312. return input.replace(/\\/g, "");
  15313. }
  15314. function makeFormatFunction(format) {
  15315. var array = format.match(formattingTokens), i, length;
  15316. for (i = 0, length = array.length; i < length; i++) {
  15317. if (formatTokenFunctions[array[i]]) {
  15318. array[i] = formatTokenFunctions[array[i]];
  15319. } else {
  15320. array[i] = removeFormattingTokens(array[i]);
  15321. }
  15322. }
  15323. return function (mom) {
  15324. var output = "";
  15325. for (i = 0; i < length; i++) {
  15326. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  15327. }
  15328. return output;
  15329. };
  15330. }
  15331. // format date using native date object
  15332. function formatMoment(m, format) {
  15333. if (!m.isValid()) {
  15334. return m.lang().invalidDate();
  15335. }
  15336. format = expandFormat(format, m.lang());
  15337. if (!formatFunctions[format]) {
  15338. formatFunctions[format] = makeFormatFunction(format);
  15339. }
  15340. return formatFunctions[format](m);
  15341. }
  15342. function expandFormat(format, lang) {
  15343. var i = 5;
  15344. function replaceLongDateFormatTokens(input) {
  15345. return lang.longDateFormat(input) || input;
  15346. }
  15347. localFormattingTokens.lastIndex = 0;
  15348. while (i >= 0 && localFormattingTokens.test(format)) {
  15349. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  15350. localFormattingTokens.lastIndex = 0;
  15351. i -= 1;
  15352. }
  15353. return format;
  15354. }
  15355. /************************************
  15356. Parsing
  15357. ************************************/
  15358. // get the regex to find the next token
  15359. function getParseRegexForToken(token, config) {
  15360. var a, strict = config._strict;
  15361. switch (token) {
  15362. case 'DDDD':
  15363. return parseTokenThreeDigits;
  15364. case 'YYYY':
  15365. case 'GGGG':
  15366. case 'gggg':
  15367. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  15368. case 'Y':
  15369. case 'G':
  15370. case 'g':
  15371. return parseTokenSignedNumber;
  15372. case 'YYYYYY':
  15373. case 'YYYYY':
  15374. case 'GGGGG':
  15375. case 'ggggg':
  15376. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  15377. case 'S':
  15378. if (strict) { return parseTokenOneDigit; }
  15379. /* falls through */
  15380. case 'SS':
  15381. if (strict) { return parseTokenTwoDigits; }
  15382. /* falls through */
  15383. case 'SSS':
  15384. if (strict) { return parseTokenThreeDigits; }
  15385. /* falls through */
  15386. case 'DDD':
  15387. return parseTokenOneToThreeDigits;
  15388. case 'MMM':
  15389. case 'MMMM':
  15390. case 'dd':
  15391. case 'ddd':
  15392. case 'dddd':
  15393. return parseTokenWord;
  15394. case 'a':
  15395. case 'A':
  15396. return getLangDefinition(config._l)._meridiemParse;
  15397. case 'X':
  15398. return parseTokenTimestampMs;
  15399. case 'Z':
  15400. case 'ZZ':
  15401. return parseTokenTimezone;
  15402. case 'T':
  15403. return parseTokenT;
  15404. case 'SSSS':
  15405. return parseTokenDigits;
  15406. case 'MM':
  15407. case 'DD':
  15408. case 'YY':
  15409. case 'GG':
  15410. case 'gg':
  15411. case 'HH':
  15412. case 'hh':
  15413. case 'mm':
  15414. case 'ss':
  15415. case 'ww':
  15416. case 'WW':
  15417. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  15418. case 'M':
  15419. case 'D':
  15420. case 'd':
  15421. case 'H':
  15422. case 'h':
  15423. case 'm':
  15424. case 's':
  15425. case 'w':
  15426. case 'W':
  15427. case 'e':
  15428. case 'E':
  15429. return parseTokenOneOrTwoDigits;
  15430. default :
  15431. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  15432. return a;
  15433. }
  15434. }
  15435. function timezoneMinutesFromString(string) {
  15436. string = string || "";
  15437. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  15438. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  15439. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  15440. minutes = +(parts[1] * 60) + toInt(parts[2]);
  15441. return parts[0] === '+' ? -minutes : minutes;
  15442. }
  15443. // function to convert string input to date
  15444. function addTimeToArrayFromToken(token, input, config) {
  15445. var a, datePartArray = config._a;
  15446. switch (token) {
  15447. // MONTH
  15448. case 'M' : // fall through to MM
  15449. case 'MM' :
  15450. if (input != null) {
  15451. datePartArray[MONTH] = toInt(input) - 1;
  15452. }
  15453. break;
  15454. case 'MMM' : // fall through to MMMM
  15455. case 'MMMM' :
  15456. a = getLangDefinition(config._l).monthsParse(input);
  15457. // if we didn't find a month name, mark the date as invalid.
  15458. if (a != null) {
  15459. datePartArray[MONTH] = a;
  15460. } else {
  15461. config._pf.invalidMonth = input;
  15462. }
  15463. break;
  15464. // DAY OF MONTH
  15465. case 'D' : // fall through to DD
  15466. case 'DD' :
  15467. if (input != null) {
  15468. datePartArray[DATE] = toInt(input);
  15469. }
  15470. break;
  15471. // DAY OF YEAR
  15472. case 'DDD' : // fall through to DDDD
  15473. case 'DDDD' :
  15474. if (input != null) {
  15475. config._dayOfYear = toInt(input);
  15476. }
  15477. break;
  15478. // YEAR
  15479. case 'YY' :
  15480. datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  15481. break;
  15482. case 'YYYY' :
  15483. case 'YYYYY' :
  15484. case 'YYYYYY' :
  15485. datePartArray[YEAR] = toInt(input);
  15486. break;
  15487. // AM / PM
  15488. case 'a' : // fall through to A
  15489. case 'A' :
  15490. config._isPm = getLangDefinition(config._l).isPM(input);
  15491. break;
  15492. // 24 HOUR
  15493. case 'H' : // fall through to hh
  15494. case 'HH' : // fall through to hh
  15495. case 'h' : // fall through to hh
  15496. case 'hh' :
  15497. datePartArray[HOUR] = toInt(input);
  15498. break;
  15499. // MINUTE
  15500. case 'm' : // fall through to mm
  15501. case 'mm' :
  15502. datePartArray[MINUTE] = toInt(input);
  15503. break;
  15504. // SECOND
  15505. case 's' : // fall through to ss
  15506. case 'ss' :
  15507. datePartArray[SECOND] = toInt(input);
  15508. break;
  15509. // MILLISECOND
  15510. case 'S' :
  15511. case 'SS' :
  15512. case 'SSS' :
  15513. case 'SSSS' :
  15514. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  15515. break;
  15516. // UNIX TIMESTAMP WITH MS
  15517. case 'X':
  15518. config._d = new Date(parseFloat(input) * 1000);
  15519. break;
  15520. // TIMEZONE
  15521. case 'Z' : // fall through to ZZ
  15522. case 'ZZ' :
  15523. config._useUTC = true;
  15524. config._tzm = timezoneMinutesFromString(input);
  15525. break;
  15526. case 'w':
  15527. case 'ww':
  15528. case 'W':
  15529. case 'WW':
  15530. case 'd':
  15531. case 'dd':
  15532. case 'ddd':
  15533. case 'dddd':
  15534. case 'e':
  15535. case 'E':
  15536. token = token.substr(0, 1);
  15537. /* falls through */
  15538. case 'gg':
  15539. case 'gggg':
  15540. case 'GG':
  15541. case 'GGGG':
  15542. case 'GGGGG':
  15543. token = token.substr(0, 2);
  15544. if (input) {
  15545. config._w = config._w || {};
  15546. config._w[token] = input;
  15547. }
  15548. break;
  15549. }
  15550. }
  15551. // convert an array to a date.
  15552. // the array should mirror the parameters below
  15553. // note: all values past the year are optional and will default to the lowest possible value.
  15554. // [year, month, day , hour, minute, second, millisecond]
  15555. function dateFromConfig(config) {
  15556. var i, date, input = [], currentDate,
  15557. yearToUse, fixYear, w, temp, lang, weekday, week;
  15558. if (config._d) {
  15559. return;
  15560. }
  15561. currentDate = currentDateArray(config);
  15562. //compute day of the year from weeks and weekdays
  15563. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  15564. fixYear = function (val) {
  15565. var int_val = parseInt(val, 10);
  15566. return val ?
  15567. (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
  15568. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  15569. };
  15570. w = config._w;
  15571. if (w.GG != null || w.W != null || w.E != null) {
  15572. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  15573. }
  15574. else {
  15575. lang = getLangDefinition(config._l);
  15576. weekday = w.d != null ? parseWeekday(w.d, lang) :
  15577. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  15578. week = parseInt(w.w, 10) || 1;
  15579. //if we're parsing 'd', then the low day numbers may be next week
  15580. if (w.d != null && weekday < lang._week.dow) {
  15581. week++;
  15582. }
  15583. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  15584. }
  15585. config._a[YEAR] = temp.year;
  15586. config._dayOfYear = temp.dayOfYear;
  15587. }
  15588. //if the day of the year is set, figure out what it is
  15589. if (config._dayOfYear) {
  15590. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  15591. if (config._dayOfYear > daysInYear(yearToUse)) {
  15592. config._pf._overflowDayOfYear = true;
  15593. }
  15594. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  15595. config._a[MONTH] = date.getUTCMonth();
  15596. config._a[DATE] = date.getUTCDate();
  15597. }
  15598. // Default to current date.
  15599. // * if no year, month, day of month are given, default to today
  15600. // * if day of month is given, default month and year
  15601. // * if month is given, default only year
  15602. // * if year is given, don't default anything
  15603. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  15604. config._a[i] = input[i] = currentDate[i];
  15605. }
  15606. // Zero out whatever was not defaulted, including time
  15607. for (; i < 7; i++) {
  15608. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  15609. }
  15610. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  15611. input[HOUR] += toInt((config._tzm || 0) / 60);
  15612. input[MINUTE] += toInt((config._tzm || 0) % 60);
  15613. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  15614. }
  15615. function dateFromObject(config) {
  15616. var normalizedInput;
  15617. if (config._d) {
  15618. return;
  15619. }
  15620. normalizedInput = normalizeObjectUnits(config._i);
  15621. config._a = [
  15622. normalizedInput.year,
  15623. normalizedInput.month,
  15624. normalizedInput.day,
  15625. normalizedInput.hour,
  15626. normalizedInput.minute,
  15627. normalizedInput.second,
  15628. normalizedInput.millisecond
  15629. ];
  15630. dateFromConfig(config);
  15631. }
  15632. function currentDateArray(config) {
  15633. var now = new Date();
  15634. if (config._useUTC) {
  15635. return [
  15636. now.getUTCFullYear(),
  15637. now.getUTCMonth(),
  15638. now.getUTCDate()
  15639. ];
  15640. } else {
  15641. return [now.getFullYear(), now.getMonth(), now.getDate()];
  15642. }
  15643. }
  15644. // date from string and format string
  15645. function makeDateFromStringAndFormat(config) {
  15646. config._a = [];
  15647. config._pf.empty = true;
  15648. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  15649. var lang = getLangDefinition(config._l),
  15650. string = '' + config._i,
  15651. i, parsedInput, tokens, token, skipped,
  15652. stringLength = string.length,
  15653. totalParsedInputLength = 0;
  15654. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  15655. for (i = 0; i < tokens.length; i++) {
  15656. token = tokens[i];
  15657. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  15658. if (parsedInput) {
  15659. skipped = string.substr(0, string.indexOf(parsedInput));
  15660. if (skipped.length > 0) {
  15661. config._pf.unusedInput.push(skipped);
  15662. }
  15663. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  15664. totalParsedInputLength += parsedInput.length;
  15665. }
  15666. // don't parse if it's not a known token
  15667. if (formatTokenFunctions[token]) {
  15668. if (parsedInput) {
  15669. config._pf.empty = false;
  15670. }
  15671. else {
  15672. config._pf.unusedTokens.push(token);
  15673. }
  15674. addTimeToArrayFromToken(token, parsedInput, config);
  15675. }
  15676. else if (config._strict && !parsedInput) {
  15677. config._pf.unusedTokens.push(token);
  15678. }
  15679. }
  15680. // add remaining unparsed input length to the string
  15681. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  15682. if (string.length > 0) {
  15683. config._pf.unusedInput.push(string);
  15684. }
  15685. // handle am pm
  15686. if (config._isPm && config._a[HOUR] < 12) {
  15687. config._a[HOUR] += 12;
  15688. }
  15689. // if is 12 am, change hours to 0
  15690. if (config._isPm === false && config._a[HOUR] === 12) {
  15691. config._a[HOUR] = 0;
  15692. }
  15693. dateFromConfig(config);
  15694. checkOverflow(config);
  15695. }
  15696. function unescapeFormat(s) {
  15697. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  15698. return p1 || p2 || p3 || p4;
  15699. });
  15700. }
  15701. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  15702. function regexpEscape(s) {
  15703. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  15704. }
  15705. // date from string and array of format strings
  15706. function makeDateFromStringAndArray(config) {
  15707. var tempConfig,
  15708. bestMoment,
  15709. scoreToBeat,
  15710. i,
  15711. currentScore;
  15712. if (config._f.length === 0) {
  15713. config._pf.invalidFormat = true;
  15714. config._d = new Date(NaN);
  15715. return;
  15716. }
  15717. for (i = 0; i < config._f.length; i++) {
  15718. currentScore = 0;
  15719. tempConfig = extend({}, config);
  15720. tempConfig._pf = defaultParsingFlags();
  15721. tempConfig._f = config._f[i];
  15722. makeDateFromStringAndFormat(tempConfig);
  15723. if (!isValid(tempConfig)) {
  15724. continue;
  15725. }
  15726. // if there is any input that was not parsed add a penalty for that format
  15727. currentScore += tempConfig._pf.charsLeftOver;
  15728. //or tokens
  15729. currentScore += tempConfig._pf.unusedTokens.length * 10;
  15730. tempConfig._pf.score = currentScore;
  15731. if (scoreToBeat == null || currentScore < scoreToBeat) {
  15732. scoreToBeat = currentScore;
  15733. bestMoment = tempConfig;
  15734. }
  15735. }
  15736. extend(config, bestMoment || tempConfig);
  15737. }
  15738. // date from iso format
  15739. function makeDateFromString(config) {
  15740. var i, l,
  15741. string = config._i,
  15742. match = isoRegex.exec(string);
  15743. if (match) {
  15744. config._pf.iso = true;
  15745. for (i = 0, l = isoDates.length; i < l; i++) {
  15746. if (isoDates[i][1].exec(string)) {
  15747. // match[5] should be "T" or undefined
  15748. config._f = isoDates[i][0] + (match[6] || " ");
  15749. break;
  15750. }
  15751. }
  15752. for (i = 0, l = isoTimes.length; i < l; i++) {
  15753. if (isoTimes[i][1].exec(string)) {
  15754. config._f += isoTimes[i][0];
  15755. break;
  15756. }
  15757. }
  15758. if (string.match(parseTokenTimezone)) {
  15759. config._f += "Z";
  15760. }
  15761. makeDateFromStringAndFormat(config);
  15762. }
  15763. else {
  15764. config._d = new Date(string);
  15765. }
  15766. }
  15767. function makeDateFromInput(config) {
  15768. var input = config._i,
  15769. matched = aspNetJsonRegex.exec(input);
  15770. if (input === undefined) {
  15771. config._d = new Date();
  15772. } else if (matched) {
  15773. config._d = new Date(+matched[1]);
  15774. } else if (typeof input === 'string') {
  15775. makeDateFromString(config);
  15776. } else if (isArray(input)) {
  15777. config._a = input.slice(0);
  15778. dateFromConfig(config);
  15779. } else if (isDate(input)) {
  15780. config._d = new Date(+input);
  15781. } else if (typeof(input) === 'object') {
  15782. dateFromObject(config);
  15783. } else {
  15784. config._d = new Date(input);
  15785. }
  15786. }
  15787. function makeDate(y, m, d, h, M, s, ms) {
  15788. //can't just apply() to create a date:
  15789. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  15790. var date = new Date(y, m, d, h, M, s, ms);
  15791. //the date constructor doesn't accept years < 1970
  15792. if (y < 1970) {
  15793. date.setFullYear(y);
  15794. }
  15795. return date;
  15796. }
  15797. function makeUTCDate(y) {
  15798. var date = new Date(Date.UTC.apply(null, arguments));
  15799. if (y < 1970) {
  15800. date.setUTCFullYear(y);
  15801. }
  15802. return date;
  15803. }
  15804. function parseWeekday(input, language) {
  15805. if (typeof input === 'string') {
  15806. if (!isNaN(input)) {
  15807. input = parseInt(input, 10);
  15808. }
  15809. else {
  15810. input = language.weekdaysParse(input);
  15811. if (typeof input !== 'number') {
  15812. return null;
  15813. }
  15814. }
  15815. }
  15816. return input;
  15817. }
  15818. /************************************
  15819. Relative Time
  15820. ************************************/
  15821. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  15822. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  15823. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  15824. }
  15825. function relativeTime(milliseconds, withoutSuffix, lang) {
  15826. var seconds = round(Math.abs(milliseconds) / 1000),
  15827. minutes = round(seconds / 60),
  15828. hours = round(minutes / 60),
  15829. days = round(hours / 24),
  15830. years = round(days / 365),
  15831. args = seconds < 45 && ['s', seconds] ||
  15832. minutes === 1 && ['m'] ||
  15833. minutes < 45 && ['mm', minutes] ||
  15834. hours === 1 && ['h'] ||
  15835. hours < 22 && ['hh', hours] ||
  15836. days === 1 && ['d'] ||
  15837. days <= 25 && ['dd', days] ||
  15838. days <= 45 && ['M'] ||
  15839. days < 345 && ['MM', round(days / 30)] ||
  15840. years === 1 && ['y'] || ['yy', years];
  15841. args[2] = withoutSuffix;
  15842. args[3] = milliseconds > 0;
  15843. args[4] = lang;
  15844. return substituteTimeAgo.apply({}, args);
  15845. }
  15846. /************************************
  15847. Week of Year
  15848. ************************************/
  15849. // firstDayOfWeek 0 = sun, 6 = sat
  15850. // the day of the week that starts the week
  15851. // (usually sunday or monday)
  15852. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  15853. // the first week is the week that contains the first
  15854. // of this day of the week
  15855. // (eg. ISO weeks use thursday (4))
  15856. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  15857. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  15858. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  15859. adjustedMoment;
  15860. if (daysToDayOfWeek > end) {
  15861. daysToDayOfWeek -= 7;
  15862. }
  15863. if (daysToDayOfWeek < end - 7) {
  15864. daysToDayOfWeek += 7;
  15865. }
  15866. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  15867. return {
  15868. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  15869. year: adjustedMoment.year()
  15870. };
  15871. }
  15872. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  15873. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  15874. var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear;
  15875. weekday = weekday != null ? weekday : firstDayOfWeek;
  15876. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0);
  15877. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  15878. return {
  15879. year: dayOfYear > 0 ? year : year - 1,
  15880. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  15881. };
  15882. }
  15883. /************************************
  15884. Top Level Functions
  15885. ************************************/
  15886. function makeMoment(config) {
  15887. var input = config._i,
  15888. format = config._f;
  15889. if (input === null) {
  15890. return moment.invalid({nullInput: true});
  15891. }
  15892. if (typeof input === 'string') {
  15893. config._i = input = getLangDefinition().preparse(input);
  15894. }
  15895. if (moment.isMoment(input)) {
  15896. config = cloneMoment(input);
  15897. config._d = new Date(+input._d);
  15898. } else if (format) {
  15899. if (isArray(format)) {
  15900. makeDateFromStringAndArray(config);
  15901. } else {
  15902. makeDateFromStringAndFormat(config);
  15903. }
  15904. } else {
  15905. makeDateFromInput(config);
  15906. }
  15907. return new Moment(config);
  15908. }
  15909. moment = function (input, format, lang, strict) {
  15910. var c;
  15911. if (typeof(lang) === "boolean") {
  15912. strict = lang;
  15913. lang = undefined;
  15914. }
  15915. // object construction must be done this way.
  15916. // https://github.com/moment/moment/issues/1423
  15917. c = {};
  15918. c._isAMomentObject = true;
  15919. c._i = input;
  15920. c._f = format;
  15921. c._l = lang;
  15922. c._strict = strict;
  15923. c._isUTC = false;
  15924. c._pf = defaultParsingFlags();
  15925. return makeMoment(c);
  15926. };
  15927. // creating with utc
  15928. moment.utc = function (input, format, lang, strict) {
  15929. var c;
  15930. if (typeof(lang) === "boolean") {
  15931. strict = lang;
  15932. lang = undefined;
  15933. }
  15934. // object construction must be done this way.
  15935. // https://github.com/moment/moment/issues/1423
  15936. c = {};
  15937. c._isAMomentObject = true;
  15938. c._useUTC = true;
  15939. c._isUTC = true;
  15940. c._l = lang;
  15941. c._i = input;
  15942. c._f = format;
  15943. c._strict = strict;
  15944. c._pf = defaultParsingFlags();
  15945. return makeMoment(c).utc();
  15946. };
  15947. // creating with unix timestamp (in seconds)
  15948. moment.unix = function (input) {
  15949. return moment(input * 1000);
  15950. };
  15951. // duration
  15952. moment.duration = function (input, key) {
  15953. var duration = input,
  15954. // matching against regexp is expensive, do it on demand
  15955. match = null,
  15956. sign,
  15957. ret,
  15958. parseIso;
  15959. if (moment.isDuration(input)) {
  15960. duration = {
  15961. ms: input._milliseconds,
  15962. d: input._days,
  15963. M: input._months
  15964. };
  15965. } else if (typeof input === 'number') {
  15966. duration = {};
  15967. if (key) {
  15968. duration[key] = input;
  15969. } else {
  15970. duration.milliseconds = input;
  15971. }
  15972. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  15973. sign = (match[1] === "-") ? -1 : 1;
  15974. duration = {
  15975. y: 0,
  15976. d: toInt(match[DATE]) * sign,
  15977. h: toInt(match[HOUR]) * sign,
  15978. m: toInt(match[MINUTE]) * sign,
  15979. s: toInt(match[SECOND]) * sign,
  15980. ms: toInt(match[MILLISECOND]) * sign
  15981. };
  15982. } else if (!!(match = isoDurationRegex.exec(input))) {
  15983. sign = (match[1] === "-") ? -1 : 1;
  15984. parseIso = function (inp) {
  15985. // We'd normally use ~~inp for this, but unfortunately it also
  15986. // converts floats to ints.
  15987. // inp may be undefined, so careful calling replace on it.
  15988. var res = inp && parseFloat(inp.replace(',', '.'));
  15989. // apply sign while we're at it
  15990. return (isNaN(res) ? 0 : res) * sign;
  15991. };
  15992. duration = {
  15993. y: parseIso(match[2]),
  15994. M: parseIso(match[3]),
  15995. d: parseIso(match[4]),
  15996. h: parseIso(match[5]),
  15997. m: parseIso(match[6]),
  15998. s: parseIso(match[7]),
  15999. w: parseIso(match[8])
  16000. };
  16001. }
  16002. ret = new Duration(duration);
  16003. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  16004. ret._lang = input._lang;
  16005. }
  16006. return ret;
  16007. };
  16008. // version number
  16009. moment.version = VERSION;
  16010. // default format
  16011. moment.defaultFormat = isoFormat;
  16012. // This function will be called whenever a moment is mutated.
  16013. // It is intended to keep the offset in sync with the timezone.
  16014. moment.updateOffset = function () {};
  16015. // This function will load languages and then set the global language. If
  16016. // no arguments are passed in, it will simply return the current global
  16017. // language key.
  16018. moment.lang = function (key, values) {
  16019. var r;
  16020. if (!key) {
  16021. return moment.fn._lang._abbr;
  16022. }
  16023. if (values) {
  16024. loadLang(normalizeLanguage(key), values);
  16025. } else if (values === null) {
  16026. unloadLang(key);
  16027. key = 'en';
  16028. } else if (!languages[key]) {
  16029. getLangDefinition(key);
  16030. }
  16031. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  16032. return r._abbr;
  16033. };
  16034. // returns language data
  16035. moment.langData = function (key) {
  16036. if (key && key._lang && key._lang._abbr) {
  16037. key = key._lang._abbr;
  16038. }
  16039. return getLangDefinition(key);
  16040. };
  16041. // compare moment object
  16042. moment.isMoment = function (obj) {
  16043. return obj instanceof Moment ||
  16044. (obj != null && obj.hasOwnProperty('_isAMomentObject'));
  16045. };
  16046. // for typechecking Duration objects
  16047. moment.isDuration = function (obj) {
  16048. return obj instanceof Duration;
  16049. };
  16050. for (i = lists.length - 1; i >= 0; --i) {
  16051. makeList(lists[i]);
  16052. }
  16053. moment.normalizeUnits = function (units) {
  16054. return normalizeUnits(units);
  16055. };
  16056. moment.invalid = function (flags) {
  16057. var m = moment.utc(NaN);
  16058. if (flags != null) {
  16059. extend(m._pf, flags);
  16060. }
  16061. else {
  16062. m._pf.userInvalidated = true;
  16063. }
  16064. return m;
  16065. };
  16066. moment.parseZone = function (input) {
  16067. return moment(input).parseZone();
  16068. };
  16069. /************************************
  16070. Moment Prototype
  16071. ************************************/
  16072. extend(moment.fn = Moment.prototype, {
  16073. clone : function () {
  16074. return moment(this);
  16075. },
  16076. valueOf : function () {
  16077. return +this._d + ((this._offset || 0) * 60000);
  16078. },
  16079. unix : function () {
  16080. return Math.floor(+this / 1000);
  16081. },
  16082. toString : function () {
  16083. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  16084. },
  16085. toDate : function () {
  16086. return this._offset ? new Date(+this) : this._d;
  16087. },
  16088. toISOString : function () {
  16089. var m = moment(this).utc();
  16090. if (0 < m.year() && m.year() <= 9999) {
  16091. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  16092. } else {
  16093. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  16094. }
  16095. },
  16096. toArray : function () {
  16097. var m = this;
  16098. return [
  16099. m.year(),
  16100. m.month(),
  16101. m.date(),
  16102. m.hours(),
  16103. m.minutes(),
  16104. m.seconds(),
  16105. m.milliseconds()
  16106. ];
  16107. },
  16108. isValid : function () {
  16109. return isValid(this);
  16110. },
  16111. isDSTShifted : function () {
  16112. if (this._a) {
  16113. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  16114. }
  16115. return false;
  16116. },
  16117. parsingFlags : function () {
  16118. return extend({}, this._pf);
  16119. },
  16120. invalidAt: function () {
  16121. return this._pf.overflow;
  16122. },
  16123. utc : function () {
  16124. return this.zone(0);
  16125. },
  16126. local : function () {
  16127. this.zone(0);
  16128. this._isUTC = false;
  16129. return this;
  16130. },
  16131. format : function (inputString) {
  16132. var output = formatMoment(this, inputString || moment.defaultFormat);
  16133. return this.lang().postformat(output);
  16134. },
  16135. add : function (input, val) {
  16136. var dur;
  16137. // switch args to support add('s', 1) and add(1, 's')
  16138. if (typeof input === 'string') {
  16139. dur = moment.duration(+val, input);
  16140. } else {
  16141. dur = moment.duration(input, val);
  16142. }
  16143. addOrSubtractDurationFromMoment(this, dur, 1);
  16144. return this;
  16145. },
  16146. subtract : function (input, val) {
  16147. var dur;
  16148. // switch args to support subtract('s', 1) and subtract(1, 's')
  16149. if (typeof input === 'string') {
  16150. dur = moment.duration(+val, input);
  16151. } else {
  16152. dur = moment.duration(input, val);
  16153. }
  16154. addOrSubtractDurationFromMoment(this, dur, -1);
  16155. return this;
  16156. },
  16157. diff : function (input, units, asFloat) {
  16158. var that = makeAs(input, this),
  16159. zoneDiff = (this.zone() - that.zone()) * 6e4,
  16160. diff, output;
  16161. units = normalizeUnits(units);
  16162. if (units === 'year' || units === 'month') {
  16163. // average number of days in the months in the given dates
  16164. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  16165. // difference in months
  16166. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  16167. // adjust by taking difference in days, average number of days
  16168. // and dst in the given months.
  16169. output += ((this - moment(this).startOf('month')) -
  16170. (that - moment(that).startOf('month'))) / diff;
  16171. // same as above but with zones, to negate all dst
  16172. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  16173. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  16174. if (units === 'year') {
  16175. output = output / 12;
  16176. }
  16177. } else {
  16178. diff = (this - that);
  16179. output = units === 'second' ? diff / 1e3 : // 1000
  16180. units === 'minute' ? diff / 6e4 : // 1000 * 60
  16181. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  16182. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  16183. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  16184. diff;
  16185. }
  16186. return asFloat ? output : absRound(output);
  16187. },
  16188. from : function (time, withoutSuffix) {
  16189. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  16190. },
  16191. fromNow : function (withoutSuffix) {
  16192. return this.from(moment(), withoutSuffix);
  16193. },
  16194. calendar : function () {
  16195. // We want to compare the start of today, vs this.
  16196. // Getting start-of-today depends on whether we're zone'd or not.
  16197. var sod = makeAs(moment(), this).startOf('day'),
  16198. diff = this.diff(sod, 'days', true),
  16199. format = diff < -6 ? 'sameElse' :
  16200. diff < -1 ? 'lastWeek' :
  16201. diff < 0 ? 'lastDay' :
  16202. diff < 1 ? 'sameDay' :
  16203. diff < 2 ? 'nextDay' :
  16204. diff < 7 ? 'nextWeek' : 'sameElse';
  16205. return this.format(this.lang().calendar(format, this));
  16206. },
  16207. isLeapYear : function () {
  16208. return isLeapYear(this.year());
  16209. },
  16210. isDST : function () {
  16211. return (this.zone() < this.clone().month(0).zone() ||
  16212. this.zone() < this.clone().month(5).zone());
  16213. },
  16214. day : function (input) {
  16215. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  16216. if (input != null) {
  16217. input = parseWeekday(input, this.lang());
  16218. return this.add({ d : input - day });
  16219. } else {
  16220. return day;
  16221. }
  16222. },
  16223. month : function (input) {
  16224. var utc = this._isUTC ? 'UTC' : '',
  16225. dayOfMonth;
  16226. if (input != null) {
  16227. if (typeof input === 'string') {
  16228. input = this.lang().monthsParse(input);
  16229. if (typeof input !== 'number') {
  16230. return this;
  16231. }
  16232. }
  16233. dayOfMonth = this.date();
  16234. this.date(1);
  16235. this._d['set' + utc + 'Month'](input);
  16236. this.date(Math.min(dayOfMonth, this.daysInMonth()));
  16237. moment.updateOffset(this);
  16238. return this;
  16239. } else {
  16240. return this._d['get' + utc + 'Month']();
  16241. }
  16242. },
  16243. startOf: function (units) {
  16244. units = normalizeUnits(units);
  16245. // the following switch intentionally omits break keywords
  16246. // to utilize falling through the cases.
  16247. switch (units) {
  16248. case 'year':
  16249. this.month(0);
  16250. /* falls through */
  16251. case 'month':
  16252. this.date(1);
  16253. /* falls through */
  16254. case 'week':
  16255. case 'isoWeek':
  16256. case 'day':
  16257. this.hours(0);
  16258. /* falls through */
  16259. case 'hour':
  16260. this.minutes(0);
  16261. /* falls through */
  16262. case 'minute':
  16263. this.seconds(0);
  16264. /* falls through */
  16265. case 'second':
  16266. this.milliseconds(0);
  16267. /* falls through */
  16268. }
  16269. // weeks are a special case
  16270. if (units === 'week') {
  16271. this.weekday(0);
  16272. } else if (units === 'isoWeek') {
  16273. this.isoWeekday(1);
  16274. }
  16275. return this;
  16276. },
  16277. endOf: function (units) {
  16278. units = normalizeUnits(units);
  16279. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  16280. },
  16281. isAfter: function (input, units) {
  16282. units = typeof units !== 'undefined' ? units : 'millisecond';
  16283. return +this.clone().startOf(units) > +moment(input).startOf(units);
  16284. },
  16285. isBefore: function (input, units) {
  16286. units = typeof units !== 'undefined' ? units : 'millisecond';
  16287. return +this.clone().startOf(units) < +moment(input).startOf(units);
  16288. },
  16289. isSame: function (input, units) {
  16290. units = units || 'ms';
  16291. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  16292. },
  16293. min: function (other) {
  16294. other = moment.apply(null, arguments);
  16295. return other < this ? this : other;
  16296. },
  16297. max: function (other) {
  16298. other = moment.apply(null, arguments);
  16299. return other > this ? this : other;
  16300. },
  16301. zone : function (input) {
  16302. var offset = this._offset || 0;
  16303. if (input != null) {
  16304. if (typeof input === "string") {
  16305. input = timezoneMinutesFromString(input);
  16306. }
  16307. if (Math.abs(input) < 16) {
  16308. input = input * 60;
  16309. }
  16310. this._offset = input;
  16311. this._isUTC = true;
  16312. if (offset !== input) {
  16313. addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
  16314. }
  16315. } else {
  16316. return this._isUTC ? offset : this._d.getTimezoneOffset();
  16317. }
  16318. return this;
  16319. },
  16320. zoneAbbr : function () {
  16321. return this._isUTC ? "UTC" : "";
  16322. },
  16323. zoneName : function () {
  16324. return this._isUTC ? "Coordinated Universal Time" : "";
  16325. },
  16326. parseZone : function () {
  16327. if (this._tzm) {
  16328. this.zone(this._tzm);
  16329. } else if (typeof this._i === 'string') {
  16330. this.zone(this._i);
  16331. }
  16332. return this;
  16333. },
  16334. hasAlignedHourOffset : function (input) {
  16335. if (!input) {
  16336. input = 0;
  16337. }
  16338. else {
  16339. input = moment(input).zone();
  16340. }
  16341. return (this.zone() - input) % 60 === 0;
  16342. },
  16343. daysInMonth : function () {
  16344. return daysInMonth(this.year(), this.month());
  16345. },
  16346. dayOfYear : function (input) {
  16347. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  16348. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  16349. },
  16350. quarter : function () {
  16351. return Math.ceil((this.month() + 1.0) / 3.0);
  16352. },
  16353. weekYear : function (input) {
  16354. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  16355. return input == null ? year : this.add("y", (input - year));
  16356. },
  16357. isoWeekYear : function (input) {
  16358. var year = weekOfYear(this, 1, 4).year;
  16359. return input == null ? year : this.add("y", (input - year));
  16360. },
  16361. week : function (input) {
  16362. var week = this.lang().week(this);
  16363. return input == null ? week : this.add("d", (input - week) * 7);
  16364. },
  16365. isoWeek : function (input) {
  16366. var week = weekOfYear(this, 1, 4).week;
  16367. return input == null ? week : this.add("d", (input - week) * 7);
  16368. },
  16369. weekday : function (input) {
  16370. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  16371. return input == null ? weekday : this.add("d", input - weekday);
  16372. },
  16373. isoWeekday : function (input) {
  16374. // behaves the same as moment#day except
  16375. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  16376. // as a setter, sunday should belong to the previous week.
  16377. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  16378. },
  16379. get : function (units) {
  16380. units = normalizeUnits(units);
  16381. return this[units]();
  16382. },
  16383. set : function (units, value) {
  16384. units = normalizeUnits(units);
  16385. if (typeof this[units] === 'function') {
  16386. this[units](value);
  16387. }
  16388. return this;
  16389. },
  16390. // If passed a language key, it will set the language for this
  16391. // instance. Otherwise, it will return the language configuration
  16392. // variables for this instance.
  16393. lang : function (key) {
  16394. if (key === undefined) {
  16395. return this._lang;
  16396. } else {
  16397. this._lang = getLangDefinition(key);
  16398. return this;
  16399. }
  16400. }
  16401. });
  16402. // helper for adding shortcuts
  16403. function makeGetterAndSetter(name, key) {
  16404. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  16405. var utc = this._isUTC ? 'UTC' : '';
  16406. if (input != null) {
  16407. this._d['set' + utc + key](input);
  16408. moment.updateOffset(this);
  16409. return this;
  16410. } else {
  16411. return this._d['get' + utc + key]();
  16412. }
  16413. };
  16414. }
  16415. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  16416. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  16417. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  16418. }
  16419. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  16420. makeGetterAndSetter('year', 'FullYear');
  16421. // add plural methods
  16422. moment.fn.days = moment.fn.day;
  16423. moment.fn.months = moment.fn.month;
  16424. moment.fn.weeks = moment.fn.week;
  16425. moment.fn.isoWeeks = moment.fn.isoWeek;
  16426. // add aliased format methods
  16427. moment.fn.toJSON = moment.fn.toISOString;
  16428. /************************************
  16429. Duration Prototype
  16430. ************************************/
  16431. extend(moment.duration.fn = Duration.prototype, {
  16432. _bubble : function () {
  16433. var milliseconds = this._milliseconds,
  16434. days = this._days,
  16435. months = this._months,
  16436. data = this._data,
  16437. seconds, minutes, hours, years;
  16438. // The following code bubbles up values, see the tests for
  16439. // examples of what that means.
  16440. data.milliseconds = milliseconds % 1000;
  16441. seconds = absRound(milliseconds / 1000);
  16442. data.seconds = seconds % 60;
  16443. minutes = absRound(seconds / 60);
  16444. data.minutes = minutes % 60;
  16445. hours = absRound(minutes / 60);
  16446. data.hours = hours % 24;
  16447. days += absRound(hours / 24);
  16448. data.days = days % 30;
  16449. months += absRound(days / 30);
  16450. data.months = months % 12;
  16451. years = absRound(months / 12);
  16452. data.years = years;
  16453. },
  16454. weeks : function () {
  16455. return absRound(this.days() / 7);
  16456. },
  16457. valueOf : function () {
  16458. return this._milliseconds +
  16459. this._days * 864e5 +
  16460. (this._months % 12) * 2592e6 +
  16461. toInt(this._months / 12) * 31536e6;
  16462. },
  16463. humanize : function (withSuffix) {
  16464. var difference = +this,
  16465. output = relativeTime(difference, !withSuffix, this.lang());
  16466. if (withSuffix) {
  16467. output = this.lang().pastFuture(difference, output);
  16468. }
  16469. return this.lang().postformat(output);
  16470. },
  16471. add : function (input, val) {
  16472. // supports only 2.0-style add(1, 's') or add(moment)
  16473. var dur = moment.duration(input, val);
  16474. this._milliseconds += dur._milliseconds;
  16475. this._days += dur._days;
  16476. this._months += dur._months;
  16477. this._bubble();
  16478. return this;
  16479. },
  16480. subtract : function (input, val) {
  16481. var dur = moment.duration(input, val);
  16482. this._milliseconds -= dur._milliseconds;
  16483. this._days -= dur._days;
  16484. this._months -= dur._months;
  16485. this._bubble();
  16486. return this;
  16487. },
  16488. get : function (units) {
  16489. units = normalizeUnits(units);
  16490. return this[units.toLowerCase() + 's']();
  16491. },
  16492. as : function (units) {
  16493. units = normalizeUnits(units);
  16494. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  16495. },
  16496. lang : moment.fn.lang,
  16497. toIsoString : function () {
  16498. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  16499. var years = Math.abs(this.years()),
  16500. months = Math.abs(this.months()),
  16501. days = Math.abs(this.days()),
  16502. hours = Math.abs(this.hours()),
  16503. minutes = Math.abs(this.minutes()),
  16504. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  16505. if (!this.asSeconds()) {
  16506. // this is the same as C#'s (Noda) and python (isodate)...
  16507. // but not other JS (goog.date)
  16508. return 'P0D';
  16509. }
  16510. return (this.asSeconds() < 0 ? '-' : '') +
  16511. 'P' +
  16512. (years ? years + 'Y' : '') +
  16513. (months ? months + 'M' : '') +
  16514. (days ? days + 'D' : '') +
  16515. ((hours || minutes || seconds) ? 'T' : '') +
  16516. (hours ? hours + 'H' : '') +
  16517. (minutes ? minutes + 'M' : '') +
  16518. (seconds ? seconds + 'S' : '');
  16519. }
  16520. });
  16521. function makeDurationGetter(name) {
  16522. moment.duration.fn[name] = function () {
  16523. return this._data[name];
  16524. };
  16525. }
  16526. function makeDurationAsGetter(name, factor) {
  16527. moment.duration.fn['as' + name] = function () {
  16528. return +this / factor;
  16529. };
  16530. }
  16531. for (i in unitMillisecondFactors) {
  16532. if (unitMillisecondFactors.hasOwnProperty(i)) {
  16533. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  16534. makeDurationGetter(i.toLowerCase());
  16535. }
  16536. }
  16537. makeDurationAsGetter('Weeks', 6048e5);
  16538. moment.duration.fn.asMonths = function () {
  16539. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  16540. };
  16541. /************************************
  16542. Default Lang
  16543. ************************************/
  16544. // Set default language, other languages will inherit from English.
  16545. moment.lang('en', {
  16546. ordinal : function (number) {
  16547. var b = number % 10,
  16548. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  16549. (b === 1) ? 'st' :
  16550. (b === 2) ? 'nd' :
  16551. (b === 3) ? 'rd' : 'th';
  16552. return number + output;
  16553. }
  16554. });
  16555. /* EMBED_LANGUAGES */
  16556. /************************************
  16557. Exposing Moment
  16558. ************************************/
  16559. function makeGlobal(deprecate) {
  16560. var warned = false, local_moment = moment;
  16561. /*global ender:false */
  16562. if (typeof ender !== 'undefined') {
  16563. return;
  16564. }
  16565. // here, `this` means `window` in the browser, or `global` on the server
  16566. // add `moment` as a global object via a string identifier,
  16567. // for Closure Compiler "advanced" mode
  16568. if (deprecate) {
  16569. global.moment = function () {
  16570. if (!warned && console && console.warn) {
  16571. warned = true;
  16572. console.warn(
  16573. "Accessing Moment through the global scope is " +
  16574. "deprecated, and will be removed in an upcoming " +
  16575. "release.");
  16576. }
  16577. return local_moment.apply(null, arguments);
  16578. };
  16579. extend(global.moment, local_moment);
  16580. } else {
  16581. global['moment'] = moment;
  16582. }
  16583. }
  16584. // CommonJS module is defined
  16585. if (hasModule) {
  16586. module.exports = moment;
  16587. makeGlobal(true);
  16588. } else if (typeof define === "function" && define.amd) {
  16589. define("moment", function (require, exports, module) {
  16590. if (module.config && module.config() && module.config().noGlobal !== true) {
  16591. // If user provided noGlobal, he is aware of global
  16592. makeGlobal(module.config().noGlobal === undefined);
  16593. }
  16594. return moment;
  16595. });
  16596. } else {
  16597. makeGlobal();
  16598. }
  16599. }).call(this);
  16600. },{}],4:[function(require,module,exports){
  16601. /**
  16602. * Copyright 2012 Craig Campbell
  16603. *
  16604. * Licensed under the Apache License, Version 2.0 (the "License");
  16605. * you may not use this file except in compliance with the License.
  16606. * You may obtain a copy of the License at
  16607. *
  16608. * http://www.apache.org/licenses/LICENSE-2.0
  16609. *
  16610. * Unless required by applicable law or agreed to in writing, software
  16611. * distributed under the License is distributed on an "AS IS" BASIS,
  16612. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16613. * See the License for the specific language governing permissions and
  16614. * limitations under the License.
  16615. *
  16616. * Mousetrap is a simple keyboard shortcut library for Javascript with
  16617. * no external dependencies
  16618. *
  16619. * @version 1.1.2
  16620. * @url craig.is/killing/mice
  16621. */
  16622. /**
  16623. * mapping of special keycodes to their corresponding keys
  16624. *
  16625. * everything in this dictionary cannot use keypress events
  16626. * so it has to be here to map to the correct keycodes for
  16627. * keyup/keydown events
  16628. *
  16629. * @type {Object}
  16630. */
  16631. var _MAP = {
  16632. 8: 'backspace',
  16633. 9: 'tab',
  16634. 13: 'enter',
  16635. 16: 'shift',
  16636. 17: 'ctrl',
  16637. 18: 'alt',
  16638. 20: 'capslock',
  16639. 27: 'esc',
  16640. 32: 'space',
  16641. 33: 'pageup',
  16642. 34: 'pagedown',
  16643. 35: 'end',
  16644. 36: 'home',
  16645. 37: 'left',
  16646. 38: 'up',
  16647. 39: 'right',
  16648. 40: 'down',
  16649. 45: 'ins',
  16650. 46: 'del',
  16651. 91: 'meta',
  16652. 93: 'meta',
  16653. 224: 'meta'
  16654. },
  16655. /**
  16656. * mapping for special characters so they can support
  16657. *
  16658. * this dictionary is only used incase you want to bind a
  16659. * keyup or keydown event to one of these keys
  16660. *
  16661. * @type {Object}
  16662. */
  16663. _KEYCODE_MAP = {
  16664. 106: '*',
  16665. 107: '+',
  16666. 109: '-',
  16667. 110: '.',
  16668. 111 : '/',
  16669. 186: ';',
  16670. 187: '=',
  16671. 188: ',',
  16672. 189: '-',
  16673. 190: '.',
  16674. 191: '/',
  16675. 192: '`',
  16676. 219: '[',
  16677. 220: '\\',
  16678. 221: ']',
  16679. 222: '\''
  16680. },
  16681. /**
  16682. * this is a mapping of keys that require shift on a US keypad
  16683. * back to the non shift equivelents
  16684. *
  16685. * this is so you can use keyup events with these keys
  16686. *
  16687. * note that this will only work reliably on US keyboards
  16688. *
  16689. * @type {Object}
  16690. */
  16691. _SHIFT_MAP = {
  16692. '~': '`',
  16693. '!': '1',
  16694. '@': '2',
  16695. '#': '3',
  16696. '$': '4',
  16697. '%': '5',
  16698. '^': '6',
  16699. '&': '7',
  16700. '*': '8',
  16701. '(': '9',
  16702. ')': '0',
  16703. '_': '-',
  16704. '+': '=',
  16705. ':': ';',
  16706. '\"': '\'',
  16707. '<': ',',
  16708. '>': '.',
  16709. '?': '/',
  16710. '|': '\\'
  16711. },
  16712. /**
  16713. * this is a list of special strings you can use to map
  16714. * to modifier keys when you specify your keyboard shortcuts
  16715. *
  16716. * @type {Object}
  16717. */
  16718. _SPECIAL_ALIASES = {
  16719. 'option': 'alt',
  16720. 'command': 'meta',
  16721. 'return': 'enter',
  16722. 'escape': 'esc'
  16723. },
  16724. /**
  16725. * variable to store the flipped version of _MAP from above
  16726. * needed to check if we should use keypress or not when no action
  16727. * is specified
  16728. *
  16729. * @type {Object|undefined}
  16730. */
  16731. _REVERSE_MAP,
  16732. /**
  16733. * a list of all the callbacks setup via Mousetrap.bind()
  16734. *
  16735. * @type {Object}
  16736. */
  16737. _callbacks = {},
  16738. /**
  16739. * direct map of string combinations to callbacks used for trigger()
  16740. *
  16741. * @type {Object}
  16742. */
  16743. _direct_map = {},
  16744. /**
  16745. * keeps track of what level each sequence is at since multiple
  16746. * sequences can start out with the same sequence
  16747. *
  16748. * @type {Object}
  16749. */
  16750. _sequence_levels = {},
  16751. /**
  16752. * variable to store the setTimeout call
  16753. *
  16754. * @type {null|number}
  16755. */
  16756. _reset_timer,
  16757. /**
  16758. * temporary state where we will ignore the next keyup
  16759. *
  16760. * @type {boolean|string}
  16761. */
  16762. _ignore_next_keyup = false,
  16763. /**
  16764. * are we currently inside of a sequence?
  16765. * type of action ("keyup" or "keydown" or "keypress") or false
  16766. *
  16767. * @type {boolean|string}
  16768. */
  16769. _inside_sequence = false;
  16770. /**
  16771. * loop through the f keys, f1 to f19 and add them to the map
  16772. * programatically
  16773. */
  16774. for (var i = 1; i < 20; ++i) {
  16775. _MAP[111 + i] = 'f' + i;
  16776. }
  16777. /**
  16778. * loop through to map numbers on the numeric keypad
  16779. */
  16780. for (i = 0; i <= 9; ++i) {
  16781. _MAP[i + 96] = i;
  16782. }
  16783. /**
  16784. * cross browser add event method
  16785. *
  16786. * @param {Element|HTMLDocument} object
  16787. * @param {string} type
  16788. * @param {Function} callback
  16789. * @returns void
  16790. */
  16791. function _addEvent(object, type, callback) {
  16792. if (object.addEventListener) {
  16793. return object.addEventListener(type, callback, false);
  16794. }
  16795. object.attachEvent('on' + type, callback);
  16796. }
  16797. /**
  16798. * takes the event and returns the key character
  16799. *
  16800. * @param {Event} e
  16801. * @return {string}
  16802. */
  16803. function _characterFromEvent(e) {
  16804. // for keypress events we should return the character as is
  16805. if (e.type == 'keypress') {
  16806. return String.fromCharCode(e.which);
  16807. }
  16808. // for non keypress events the special maps are needed
  16809. if (_MAP[e.which]) {
  16810. return _MAP[e.which];
  16811. }
  16812. if (_KEYCODE_MAP[e.which]) {
  16813. return _KEYCODE_MAP[e.which];
  16814. }
  16815. // if it is not in the special map
  16816. return String.fromCharCode(e.which).toLowerCase();
  16817. }
  16818. /**
  16819. * should we stop this event before firing off callbacks
  16820. *
  16821. * @param {Event} e
  16822. * @return {boolean}
  16823. */
  16824. function _stop(e) {
  16825. var element = e.target || e.srcElement,
  16826. tag_name = element.tagName;
  16827. // if the element has the class "mousetrap" then no need to stop
  16828. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  16829. return false;
  16830. }
  16831. // stop for input, select, and textarea
  16832. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  16833. }
  16834. /**
  16835. * checks if two arrays are equal
  16836. *
  16837. * @param {Array} modifiers1
  16838. * @param {Array} modifiers2
  16839. * @returns {boolean}
  16840. */
  16841. function _modifiersMatch(modifiers1, modifiers2) {
  16842. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  16843. }
  16844. /**
  16845. * resets all sequence counters except for the ones passed in
  16846. *
  16847. * @param {Object} do_not_reset
  16848. * @returns void
  16849. */
  16850. function _resetSequences(do_not_reset) {
  16851. do_not_reset = do_not_reset || {};
  16852. var active_sequences = false,
  16853. key;
  16854. for (key in _sequence_levels) {
  16855. if (do_not_reset[key]) {
  16856. active_sequences = true;
  16857. continue;
  16858. }
  16859. _sequence_levels[key] = 0;
  16860. }
  16861. if (!active_sequences) {
  16862. _inside_sequence = false;
  16863. }
  16864. }
  16865. /**
  16866. * finds all callbacks that match based on the keycode, modifiers,
  16867. * and action
  16868. *
  16869. * @param {string} character
  16870. * @param {Array} modifiers
  16871. * @param {string} action
  16872. * @param {boolean=} remove - should we remove any matches
  16873. * @param {string=} combination
  16874. * @returns {Array}
  16875. */
  16876. function _getMatches(character, modifiers, action, remove, combination) {
  16877. var i,
  16878. callback,
  16879. matches = [];
  16880. // if there are no events related to this keycode
  16881. if (!_callbacks[character]) {
  16882. return [];
  16883. }
  16884. // if a modifier key is coming up on its own we should allow it
  16885. if (action == 'keyup' && _isModifier(character)) {
  16886. modifiers = [character];
  16887. }
  16888. // loop through all callbacks for the key that was pressed
  16889. // and see if any of them match
  16890. for (i = 0; i < _callbacks[character].length; ++i) {
  16891. callback = _callbacks[character][i];
  16892. // if this is a sequence but it is not at the right level
  16893. // then move onto the next match
  16894. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  16895. continue;
  16896. }
  16897. // if the action we are looking for doesn't match the action we got
  16898. // then we should keep going
  16899. if (action != callback.action) {
  16900. continue;
  16901. }
  16902. // if this is a keypress event that means that we need to only
  16903. // look at the character, otherwise check the modifiers as
  16904. // well
  16905. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  16906. // remove is used so if you change your mind and call bind a
  16907. // second time with a new function the first one is overwritten
  16908. if (remove && callback.combo == combination) {
  16909. _callbacks[character].splice(i, 1);
  16910. }
  16911. matches.push(callback);
  16912. }
  16913. }
  16914. return matches;
  16915. }
  16916. /**
  16917. * takes a key event and figures out what the modifiers are
  16918. *
  16919. * @param {Event} e
  16920. * @returns {Array}
  16921. */
  16922. function _eventModifiers(e) {
  16923. var modifiers = [];
  16924. if (e.shiftKey) {
  16925. modifiers.push('shift');
  16926. }
  16927. if (e.altKey) {
  16928. modifiers.push('alt');
  16929. }
  16930. if (e.ctrlKey) {
  16931. modifiers.push('ctrl');
  16932. }
  16933. if (e.metaKey) {
  16934. modifiers.push('meta');
  16935. }
  16936. return modifiers;
  16937. }
  16938. /**
  16939. * actually calls the callback function
  16940. *
  16941. * if your callback function returns false this will use the jquery
  16942. * convention - prevent default and stop propogation on the event
  16943. *
  16944. * @param {Function} callback
  16945. * @param {Event} e
  16946. * @returns void
  16947. */
  16948. function _fireCallback(callback, e) {
  16949. if (callback(e) === false) {
  16950. if (e.preventDefault) {
  16951. e.preventDefault();
  16952. }
  16953. if (e.stopPropagation) {
  16954. e.stopPropagation();
  16955. }
  16956. e.returnValue = false;
  16957. e.cancelBubble = true;
  16958. }
  16959. }
  16960. /**
  16961. * handles a character key event
  16962. *
  16963. * @param {string} character
  16964. * @param {Event} e
  16965. * @returns void
  16966. */
  16967. function _handleCharacter(character, e) {
  16968. // if this event should not happen stop here
  16969. if (_stop(e)) {
  16970. return;
  16971. }
  16972. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  16973. i,
  16974. do_not_reset = {},
  16975. processed_sequence_callback = false;
  16976. // loop through matching callbacks for this key event
  16977. for (i = 0; i < callbacks.length; ++i) {
  16978. // fire for all sequence callbacks
  16979. // this is because if for example you have multiple sequences
  16980. // bound such as "g i" and "g t" they both need to fire the
  16981. // callback for matching g cause otherwise you can only ever
  16982. // match the first one
  16983. if (callbacks[i].seq) {
  16984. processed_sequence_callback = true;
  16985. // keep a list of which sequences were matches for later
  16986. do_not_reset[callbacks[i].seq] = 1;
  16987. _fireCallback(callbacks[i].callback, e);
  16988. continue;
  16989. }
  16990. // if there were no sequence matches but we are still here
  16991. // that means this is a regular match so we should fire that
  16992. if (!processed_sequence_callback && !_inside_sequence) {
  16993. _fireCallback(callbacks[i].callback, e);
  16994. }
  16995. }
  16996. // if you are inside of a sequence and the key you are pressing
  16997. // is not a modifier key then we should reset all sequences
  16998. // that were not matched by this key event
  16999. if (e.type == _inside_sequence && !_isModifier(character)) {
  17000. _resetSequences(do_not_reset);
  17001. }
  17002. }
  17003. /**
  17004. * handles a keydown event
  17005. *
  17006. * @param {Event} e
  17007. * @returns void
  17008. */
  17009. function _handleKey(e) {
  17010. // normalize e.which for key events
  17011. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  17012. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  17013. var character = _characterFromEvent(e);
  17014. // no character found then stop
  17015. if (!character) {
  17016. return;
  17017. }
  17018. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  17019. _ignore_next_keyup = false;
  17020. return;
  17021. }
  17022. _handleCharacter(character, e);
  17023. }
  17024. /**
  17025. * determines if the keycode specified is a modifier key or not
  17026. *
  17027. * @param {string} key
  17028. * @returns {boolean}
  17029. */
  17030. function _isModifier(key) {
  17031. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  17032. }
  17033. /**
  17034. * called to set a 1 second timeout on the specified sequence
  17035. *
  17036. * this is so after each key press in the sequence you have 1 second
  17037. * to press the next key before you have to start over
  17038. *
  17039. * @returns void
  17040. */
  17041. function _resetSequenceTimer() {
  17042. clearTimeout(_reset_timer);
  17043. _reset_timer = setTimeout(_resetSequences, 1000);
  17044. }
  17045. /**
  17046. * reverses the map lookup so that we can look for specific keys
  17047. * to see what can and can't use keypress
  17048. *
  17049. * @return {Object}
  17050. */
  17051. function _getReverseMap() {
  17052. if (!_REVERSE_MAP) {
  17053. _REVERSE_MAP = {};
  17054. for (var key in _MAP) {
  17055. // pull out the numeric keypad from here cause keypress should
  17056. // be able to detect the keys from the character
  17057. if (key > 95 && key < 112) {
  17058. continue;
  17059. }
  17060. if (_MAP.hasOwnProperty(key)) {
  17061. _REVERSE_MAP[_MAP[key]] = key;
  17062. }
  17063. }
  17064. }
  17065. return _REVERSE_MAP;
  17066. }
  17067. /**
  17068. * picks the best action based on the key combination
  17069. *
  17070. * @param {string} key - character for key
  17071. * @param {Array} modifiers
  17072. * @param {string=} action passed in
  17073. */
  17074. function _pickBestAction(key, modifiers, action) {
  17075. // if no action was picked in we should try to pick the one
  17076. // that we think would work best for this key
  17077. if (!action) {
  17078. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  17079. }
  17080. // modifier keys don't work as expected with keypress,
  17081. // switch to keydown
  17082. if (action == 'keypress' && modifiers.length) {
  17083. action = 'keydown';
  17084. }
  17085. return action;
  17086. }
  17087. /**
  17088. * binds a key sequence to an event
  17089. *
  17090. * @param {string} combo - combo specified in bind call
  17091. * @param {Array} keys
  17092. * @param {Function} callback
  17093. * @param {string=} action
  17094. * @returns void
  17095. */
  17096. function _bindSequence(combo, keys, callback, action) {
  17097. // start off by adding a sequence level record for this combination
  17098. // and setting the level to 0
  17099. _sequence_levels[combo] = 0;
  17100. // if there is no action pick the best one for the first key
  17101. // in the sequence
  17102. if (!action) {
  17103. action = _pickBestAction(keys[0], []);
  17104. }
  17105. /**
  17106. * callback to increase the sequence level for this sequence and reset
  17107. * all other sequences that were active
  17108. *
  17109. * @param {Event} e
  17110. * @returns void
  17111. */
  17112. var _increaseSequence = function(e) {
  17113. _inside_sequence = action;
  17114. ++_sequence_levels[combo];
  17115. _resetSequenceTimer();
  17116. },
  17117. /**
  17118. * wraps the specified callback inside of another function in order
  17119. * to reset all sequence counters as soon as this sequence is done
  17120. *
  17121. * @param {Event} e
  17122. * @returns void
  17123. */
  17124. _callbackAndReset = function(e) {
  17125. _fireCallback(callback, e);
  17126. // we should ignore the next key up if the action is key down
  17127. // or keypress. this is so if you finish a sequence and
  17128. // release the key the final key will not trigger a keyup
  17129. if (action !== 'keyup') {
  17130. _ignore_next_keyup = _characterFromEvent(e);
  17131. }
  17132. // weird race condition if a sequence ends with the key
  17133. // another sequence begins with
  17134. setTimeout(_resetSequences, 10);
  17135. },
  17136. i;
  17137. // loop through keys one at a time and bind the appropriate callback
  17138. // function. for any key leading up to the final one it should
  17139. // increase the sequence. after the final, it should reset all sequences
  17140. for (i = 0; i < keys.length; ++i) {
  17141. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  17142. }
  17143. }
  17144. /**
  17145. * binds a single keyboard combination
  17146. *
  17147. * @param {string} combination
  17148. * @param {Function} callback
  17149. * @param {string=} action
  17150. * @param {string=} sequence_name - name of sequence if part of sequence
  17151. * @param {number=} level - what part of the sequence the command is
  17152. * @returns void
  17153. */
  17154. function _bindSingle(combination, callback, action, sequence_name, level) {
  17155. // make sure multiple spaces in a row become a single space
  17156. combination = combination.replace(/\s+/g, ' ');
  17157. var sequence = combination.split(' '),
  17158. i,
  17159. key,
  17160. keys,
  17161. modifiers = [];
  17162. // if this pattern is a sequence of keys then run through this method
  17163. // to reprocess each pattern one key at a time
  17164. if (sequence.length > 1) {
  17165. return _bindSequence(combination, sequence, callback, action);
  17166. }
  17167. // take the keys from this pattern and figure out what the actual
  17168. // pattern is all about
  17169. keys = combination === '+' ? ['+'] : combination.split('+');
  17170. for (i = 0; i < keys.length; ++i) {
  17171. key = keys[i];
  17172. // normalize key names
  17173. if (_SPECIAL_ALIASES[key]) {
  17174. key = _SPECIAL_ALIASES[key];
  17175. }
  17176. // if this is not a keypress event then we should
  17177. // be smart about using shift keys
  17178. // this will only work for US keyboards however
  17179. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  17180. key = _SHIFT_MAP[key];
  17181. modifiers.push('shift');
  17182. }
  17183. // if this key is a modifier then add it to the list of modifiers
  17184. if (_isModifier(key)) {
  17185. modifiers.push(key);
  17186. }
  17187. }
  17188. // depending on what the key combination is
  17189. // we will try to pick the best event for it
  17190. action = _pickBestAction(key, modifiers, action);
  17191. // make sure to initialize array if this is the first time
  17192. // a callback is added for this key
  17193. if (!_callbacks[key]) {
  17194. _callbacks[key] = [];
  17195. }
  17196. // remove an existing match if there is one
  17197. _getMatches(key, modifiers, action, !sequence_name, combination);
  17198. // add this call back to the array
  17199. // if it is a sequence put it at the beginning
  17200. // if not put it at the end
  17201. //
  17202. // this is important because the way these are processed expects
  17203. // the sequence ones to come first
  17204. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  17205. callback: callback,
  17206. modifiers: modifiers,
  17207. action: action,
  17208. seq: sequence_name,
  17209. level: level,
  17210. combo: combination
  17211. });
  17212. }
  17213. /**
  17214. * binds multiple combinations to the same callback
  17215. *
  17216. * @param {Array} combinations
  17217. * @param {Function} callback
  17218. * @param {string|undefined} action
  17219. * @returns void
  17220. */
  17221. function _bindMultiple(combinations, callback, action) {
  17222. for (var i = 0; i < combinations.length; ++i) {
  17223. _bindSingle(combinations[i], callback, action);
  17224. }
  17225. }
  17226. // start!
  17227. _addEvent(document, 'keypress', _handleKey);
  17228. _addEvent(document, 'keydown', _handleKey);
  17229. _addEvent(document, 'keyup', _handleKey);
  17230. var mousetrap = {
  17231. /**
  17232. * binds an event to mousetrap
  17233. *
  17234. * can be a single key, a combination of keys separated with +,
  17235. * a comma separated list of keys, an array of keys, or
  17236. * a sequence of keys separated by spaces
  17237. *
  17238. * be sure to list the modifier keys first to make sure that the
  17239. * correct key ends up getting bound (the last key in the pattern)
  17240. *
  17241. * @param {string|Array} keys
  17242. * @param {Function} callback
  17243. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  17244. * @returns void
  17245. */
  17246. bind: function(keys, callback, action) {
  17247. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  17248. _direct_map[keys + ':' + action] = callback;
  17249. return this;
  17250. },
  17251. /**
  17252. * unbinds an event to mousetrap
  17253. *
  17254. * the unbinding sets the callback function of the specified key combo
  17255. * to an empty function and deletes the corresponding key in the
  17256. * _direct_map dict.
  17257. *
  17258. * the keycombo+action has to be exactly the same as
  17259. * it was defined in the bind method
  17260. *
  17261. * TODO: actually remove this from the _callbacks dictionary instead
  17262. * of binding an empty function
  17263. *
  17264. * @param {string|Array} keys
  17265. * @param {string} action
  17266. * @returns void
  17267. */
  17268. unbind: function(keys, action) {
  17269. if (_direct_map[keys + ':' + action]) {
  17270. delete _direct_map[keys + ':' + action];
  17271. this.bind(keys, function() {}, action);
  17272. }
  17273. return this;
  17274. },
  17275. /**
  17276. * triggers an event that has already been bound
  17277. *
  17278. * @param {string} keys
  17279. * @param {string=} action
  17280. * @returns void
  17281. */
  17282. trigger: function(keys, action) {
  17283. _direct_map[keys + ':' + action]();
  17284. return this;
  17285. },
  17286. /**
  17287. * resets the library back to its initial state. this is useful
  17288. * if you want to clear out the current keyboard shortcuts and bind
  17289. * new ones - for example if you switch to another page
  17290. *
  17291. * @returns void
  17292. */
  17293. reset: function() {
  17294. _callbacks = {};
  17295. _direct_map = {};
  17296. return this;
  17297. }
  17298. };
  17299. module.exports = mousetrap;
  17300. },{}]},{},[1])
  17301. (1)
  17302. });