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.

18690 lines
534 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 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-20
  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. * Event listener (singleton)
  867. */
  868. // TODO: replace usage of the event listener for the EventBus
  869. var events = {
  870. 'listeners': [],
  871. /**
  872. * Find a single listener by its object
  873. * @param {Object} object
  874. * @return {Number} index -1 when not found
  875. */
  876. 'indexOf': function (object) {
  877. var listeners = this.listeners;
  878. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  879. var listener = listeners[i];
  880. if (listener && listener.object == object) {
  881. return i;
  882. }
  883. }
  884. return -1;
  885. },
  886. /**
  887. * Add an event listener
  888. * @param {Object} object
  889. * @param {String} event The name of an event, for example 'select'
  890. * @param {function} callback The callback method, called when the
  891. * event takes place
  892. */
  893. 'addListener': function (object, event, callback) {
  894. var index = this.indexOf(object);
  895. var listener = this.listeners[index];
  896. if (!listener) {
  897. listener = {
  898. 'object': object,
  899. 'events': {}
  900. };
  901. this.listeners.push(listener);
  902. }
  903. var callbacks = listener.events[event];
  904. if (!callbacks) {
  905. callbacks = [];
  906. listener.events[event] = callbacks;
  907. }
  908. // add the callback if it does not yet exist
  909. if (callbacks.indexOf(callback) == -1) {
  910. callbacks.push(callback);
  911. }
  912. },
  913. /**
  914. * Remove an event listener
  915. * @param {Object} object
  916. * @param {String} event The name of an event, for example 'select'
  917. * @param {function} callback The registered callback method
  918. */
  919. 'removeListener': function (object, event, callback) {
  920. var index = this.indexOf(object);
  921. var listener = this.listeners[index];
  922. if (listener) {
  923. var callbacks = listener.events[event];
  924. if (callbacks) {
  925. index = callbacks.indexOf(callback);
  926. if (index != -1) {
  927. callbacks.splice(index, 1);
  928. }
  929. // remove the array when empty
  930. if (callbacks.length == 0) {
  931. delete listener.events[event];
  932. }
  933. }
  934. // count the number of registered events. remove listener when empty
  935. var count = 0;
  936. var events = listener.events;
  937. for (var e in events) {
  938. if (events.hasOwnProperty(e)) {
  939. count++;
  940. }
  941. }
  942. if (count == 0) {
  943. delete this.listeners[index];
  944. }
  945. }
  946. },
  947. /**
  948. * Remove all registered event listeners
  949. */
  950. 'removeAllListeners': function () {
  951. this.listeners = [];
  952. },
  953. /**
  954. * Trigger an event. All registered event handlers will be called
  955. * @param {Object} object
  956. * @param {String} event
  957. * @param {Object} properties (optional)
  958. */
  959. 'trigger': function (object, event, properties) {
  960. var index = this.indexOf(object);
  961. var listener = this.listeners[index];
  962. if (listener) {
  963. var callbacks = listener.events[event];
  964. if (callbacks) {
  965. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  966. callbacks[i](properties);
  967. }
  968. }
  969. }
  970. }
  971. };
  972. /**
  973. * An event bus can be used to emit events, and to subscribe to events
  974. * @constructor EventBus
  975. */
  976. function EventBus() {
  977. this.subscriptions = [];
  978. }
  979. /**
  980. * Subscribe to an event
  981. * @param {String | RegExp} event The event can be a regular expression, or
  982. * a string with wildcards, like 'server.*'.
  983. * @param {function} callback. Callback are called with three parameters:
  984. * {String} event, {*} [data], {*} [source]
  985. * @param {*} [target]
  986. * @returns {String} id A subscription id
  987. */
  988. EventBus.prototype.on = function (event, callback, target) {
  989. var regexp = (event instanceof RegExp) ?
  990. event :
  991. new RegExp(event.replace('*', '\\w+'));
  992. var subscription = {
  993. id: util.randomUUID(),
  994. event: event,
  995. regexp: regexp,
  996. callback: (typeof callback === 'function') ? callback : null,
  997. target: target
  998. };
  999. this.subscriptions.push(subscription);
  1000. return subscription.id;
  1001. };
  1002. /**
  1003. * Unsubscribe from an event
  1004. * @param {String | Object} filter Filter for subscriptions to be removed
  1005. * Filter can be a string containing a
  1006. * subscription id, or an object containing
  1007. * one or more of the fields id, event,
  1008. * callback, and target.
  1009. */
  1010. EventBus.prototype.off = function (filter) {
  1011. var i = 0;
  1012. while (i < this.subscriptions.length) {
  1013. var subscription = this.subscriptions[i];
  1014. var match = true;
  1015. if (filter instanceof Object) {
  1016. // filter is an object. All fields must match
  1017. for (var prop in filter) {
  1018. if (filter.hasOwnProperty(prop)) {
  1019. if (filter[prop] !== subscription[prop]) {
  1020. match = false;
  1021. }
  1022. }
  1023. }
  1024. }
  1025. else {
  1026. // filter is a string, filter on id
  1027. match = (subscription.id == filter);
  1028. }
  1029. if (match) {
  1030. this.subscriptions.splice(i, 1);
  1031. }
  1032. else {
  1033. i++;
  1034. }
  1035. }
  1036. };
  1037. /**
  1038. * Emit an event
  1039. * @param {String} event
  1040. * @param {*} [data]
  1041. * @param {*} [source]
  1042. */
  1043. EventBus.prototype.emit = function (event, data, source) {
  1044. for (var i =0; i < this.subscriptions.length; i++) {
  1045. var subscription = this.subscriptions[i];
  1046. if (subscription.regexp.test(event)) {
  1047. if (subscription.callback) {
  1048. subscription.callback(event, data, source);
  1049. }
  1050. }
  1051. }
  1052. };
  1053. /**
  1054. * DataSet
  1055. *
  1056. * Usage:
  1057. * var dataSet = new DataSet({
  1058. * fieldId: '_id',
  1059. * convert: {
  1060. * // ...
  1061. * }
  1062. * });
  1063. *
  1064. * dataSet.add(item);
  1065. * dataSet.add(data);
  1066. * dataSet.update(item);
  1067. * dataSet.update(data);
  1068. * dataSet.remove(id);
  1069. * dataSet.remove(ids);
  1070. * var data = dataSet.get();
  1071. * var data = dataSet.get(id);
  1072. * var data = dataSet.get(ids);
  1073. * var data = dataSet.get(ids, options, data);
  1074. * dataSet.clear();
  1075. *
  1076. * A data set can:
  1077. * - add/remove/update data
  1078. * - gives triggers upon changes in the data
  1079. * - can import/export data in various data formats
  1080. *
  1081. * @param {Object} [options] Available options:
  1082. * {String} fieldId Field name of the id in the
  1083. * items, 'id' by default.
  1084. * {Object.<String, String} convert
  1085. * A map with field names as key,
  1086. * and the field type as value.
  1087. * @constructor DataSet
  1088. */
  1089. // TODO: add a DataSet constructor DataSet(data, options)
  1090. function DataSet (options) {
  1091. this.id = util.randomUUID();
  1092. this.options = options || {};
  1093. this.data = {}; // map with data indexed by id
  1094. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  1095. this.convert = {}; // field types by field name
  1096. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  1097. if (this.options.convert) {
  1098. for (var field in this.options.convert) {
  1099. if (this.options.convert.hasOwnProperty(field)) {
  1100. var value = this.options.convert[field];
  1101. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  1102. this.convert[field] = 'Date';
  1103. }
  1104. else {
  1105. this.convert[field] = value;
  1106. }
  1107. }
  1108. }
  1109. }
  1110. // event subscribers
  1111. this.subscribers = {};
  1112. this.internalIds = {}; // internally generated id's
  1113. }
  1114. /**
  1115. * Subscribe to an event, add an event listener
  1116. * @param {String} event Event name. Available events: 'put', 'update',
  1117. * 'remove'
  1118. * @param {function} callback Callback method. Called with three parameters:
  1119. * {String} event
  1120. * {Object | null} params
  1121. * {String | Number} senderId
  1122. */
  1123. DataSet.prototype.subscribe = function (event, callback) {
  1124. var subscribers = this.subscribers[event];
  1125. if (!subscribers) {
  1126. subscribers = [];
  1127. this.subscribers[event] = subscribers;
  1128. }
  1129. subscribers.push({
  1130. callback: callback
  1131. });
  1132. };
  1133. /**
  1134. * Unsubscribe from an event, remove an event listener
  1135. * @param {String} event
  1136. * @param {function} callback
  1137. */
  1138. DataSet.prototype.unsubscribe = function (event, callback) {
  1139. var subscribers = this.subscribers[event];
  1140. if (subscribers) {
  1141. this.subscribers[event] = subscribers.filter(function (listener) {
  1142. return (listener.callback != callback);
  1143. });
  1144. }
  1145. };
  1146. /**
  1147. * Trigger an event
  1148. * @param {String} event
  1149. * @param {Object | null} params
  1150. * @param {String} [senderId] Optional id of the sender.
  1151. * @private
  1152. */
  1153. DataSet.prototype._trigger = function (event, params, senderId) {
  1154. if (event == '*') {
  1155. throw new Error('Cannot trigger event *');
  1156. }
  1157. var subscribers = [];
  1158. if (event in this.subscribers) {
  1159. subscribers = subscribers.concat(this.subscribers[event]);
  1160. }
  1161. if ('*' in this.subscribers) {
  1162. subscribers = subscribers.concat(this.subscribers['*']);
  1163. }
  1164. for (var i = 0; i < subscribers.length; i++) {
  1165. var subscriber = subscribers[i];
  1166. if (subscriber.callback) {
  1167. subscriber.callback(event, params, senderId || null);
  1168. }
  1169. }
  1170. };
  1171. /**
  1172. * Add data.
  1173. * Adding an item will fail when there already is an item with the same id.
  1174. * @param {Object | Array | DataTable} data
  1175. * @param {String} [senderId] Optional sender id
  1176. * @return {Array} addedIds Array with the ids of the added items
  1177. */
  1178. DataSet.prototype.add = function (data, senderId) {
  1179. var addedIds = [],
  1180. id,
  1181. me = this;
  1182. if (data instanceof Array) {
  1183. // Array
  1184. for (var i = 0, len = data.length; i < len; i++) {
  1185. id = me._addItem(data[i]);
  1186. addedIds.push(id);
  1187. }
  1188. }
  1189. else if (util.isDataTable(data)) {
  1190. // Google DataTable
  1191. var columns = this._getColumnNames(data);
  1192. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1193. var item = {};
  1194. for (var col = 0, cols = columns.length; col < cols; col++) {
  1195. var field = columns[col];
  1196. item[field] = data.getValue(row, col);
  1197. }
  1198. id = me._addItem(item);
  1199. addedIds.push(id);
  1200. }
  1201. }
  1202. else if (data instanceof Object) {
  1203. // Single item
  1204. id = me._addItem(data);
  1205. addedIds.push(id);
  1206. }
  1207. else {
  1208. throw new Error('Unknown dataType');
  1209. }
  1210. if (addedIds.length) {
  1211. this._trigger('add', {items: addedIds}, senderId);
  1212. }
  1213. return addedIds;
  1214. };
  1215. /**
  1216. * Update existing items. When an item does not exist, it will be created
  1217. * @param {Object | Array | DataTable} data
  1218. * @param {String} [senderId] Optional sender id
  1219. * @return {Array} updatedIds The ids of the added or updated items
  1220. */
  1221. DataSet.prototype.update = function (data, senderId) {
  1222. var addedIds = [],
  1223. updatedIds = [],
  1224. me = this,
  1225. fieldId = me.fieldId;
  1226. var addOrUpdate = function (item) {
  1227. var id = item[fieldId];
  1228. if (me.data[id]) {
  1229. // update item
  1230. id = me._updateItem(item);
  1231. updatedIds.push(id);
  1232. }
  1233. else {
  1234. // add new item
  1235. id = me._addItem(item);
  1236. addedIds.push(id);
  1237. }
  1238. };
  1239. if (data instanceof Array) {
  1240. // Array
  1241. for (var i = 0, len = data.length; i < len; i++) {
  1242. addOrUpdate(data[i]);
  1243. }
  1244. }
  1245. else if (util.isDataTable(data)) {
  1246. // Google DataTable
  1247. var columns = this._getColumnNames(data);
  1248. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  1249. var item = {};
  1250. for (var col = 0, cols = columns.length; col < cols; col++) {
  1251. var field = columns[col];
  1252. item[field] = data.getValue(row, col);
  1253. }
  1254. addOrUpdate(item);
  1255. }
  1256. }
  1257. else if (data instanceof Object) {
  1258. // Single item
  1259. addOrUpdate(data);
  1260. }
  1261. else {
  1262. throw new Error('Unknown dataType');
  1263. }
  1264. if (addedIds.length) {
  1265. this._trigger('add', {items: addedIds}, senderId);
  1266. }
  1267. if (updatedIds.length) {
  1268. this._trigger('update', {items: updatedIds}, senderId);
  1269. }
  1270. return addedIds.concat(updatedIds);
  1271. };
  1272. /**
  1273. * Get a data item or multiple items.
  1274. *
  1275. * Usage:
  1276. *
  1277. * get()
  1278. * get(options: Object)
  1279. * get(options: Object, data: Array | DataTable)
  1280. *
  1281. * get(id: Number | String)
  1282. * get(id: Number | String, options: Object)
  1283. * get(id: Number | String, options: Object, data: Array | DataTable)
  1284. *
  1285. * get(ids: Number[] | String[])
  1286. * get(ids: Number[] | String[], options: Object)
  1287. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  1288. *
  1289. * Where:
  1290. *
  1291. * {Number | String} id The id of an item
  1292. * {Number[] | String{}} ids An array with ids of items
  1293. * {Object} options An Object with options. Available options:
  1294. * {String} [type] Type of data to be returned. Can
  1295. * be 'DataTable' or 'Array' (default)
  1296. * {Object.<String, String>} [convert]
  1297. * {String[]} [fields] field names to be returned
  1298. * {function} [filter] filter items
  1299. * {String | function} [order] Order the items by
  1300. * a field name or custom sort function.
  1301. * {Array | DataTable} [data] If provided, items will be appended to this
  1302. * array or table. Required in case of Google
  1303. * DataTable.
  1304. *
  1305. * @throws Error
  1306. */
  1307. DataSet.prototype.get = function (args) {
  1308. var me = this;
  1309. var globalShowInternalIds = this.showInternalIds;
  1310. // parse the arguments
  1311. var id, ids, options, data;
  1312. var firstType = util.getType(arguments[0]);
  1313. if (firstType == 'String' || firstType == 'Number') {
  1314. // get(id [, options] [, data])
  1315. id = arguments[0];
  1316. options = arguments[1];
  1317. data = arguments[2];
  1318. }
  1319. else if (firstType == 'Array') {
  1320. // get(ids [, options] [, data])
  1321. ids = arguments[0];
  1322. options = arguments[1];
  1323. data = arguments[2];
  1324. }
  1325. else {
  1326. // get([, options] [, data])
  1327. options = arguments[0];
  1328. data = arguments[1];
  1329. }
  1330. // determine the return type
  1331. var type;
  1332. if (options && options.type) {
  1333. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  1334. if (data && (type != util.getType(data))) {
  1335. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  1336. 'does not correspond with specified options.type (' + options.type + ')');
  1337. }
  1338. if (type == 'DataTable' && !util.isDataTable(data)) {
  1339. throw new Error('Parameter "data" must be a DataTable ' +
  1340. 'when options.type is "DataTable"');
  1341. }
  1342. }
  1343. else if (data) {
  1344. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  1345. }
  1346. else {
  1347. type = 'Array';
  1348. }
  1349. // we allow the setting of this value for a single get request.
  1350. if (options != undefined) {
  1351. if (options.showInternalIds != undefined) {
  1352. this.showInternalIds = options.showInternalIds;
  1353. }
  1354. }
  1355. // build options
  1356. var convert = options && options.convert || this.options.convert;
  1357. var filter = options && options.filter;
  1358. var items = [], item, itemId, i, len;
  1359. // convert items
  1360. if (id != undefined) {
  1361. // return a single item
  1362. item = me._getItem(id, convert);
  1363. if (filter && !filter(item)) {
  1364. item = null;
  1365. }
  1366. }
  1367. else if (ids != undefined) {
  1368. // return a subset of items
  1369. for (i = 0, len = ids.length; i < len; i++) {
  1370. item = me._getItem(ids[i], convert);
  1371. if (!filter || filter(item)) {
  1372. items.push(item);
  1373. }
  1374. }
  1375. }
  1376. else {
  1377. // return all items
  1378. for (itemId in this.data) {
  1379. if (this.data.hasOwnProperty(itemId)) {
  1380. item = me._getItem(itemId, convert);
  1381. if (!filter || filter(item)) {
  1382. items.push(item);
  1383. }
  1384. }
  1385. }
  1386. }
  1387. // restore the global value of showInternalIds
  1388. this.showInternalIds = globalShowInternalIds;
  1389. // order the results
  1390. if (options && options.order && id == undefined) {
  1391. this._sort(items, options.order);
  1392. }
  1393. // filter fields of the items
  1394. if (options && options.fields) {
  1395. var fields = options.fields;
  1396. if (id != undefined) {
  1397. item = this._filterFields(item, fields);
  1398. }
  1399. else {
  1400. for (i = 0, len = items.length; i < len; i++) {
  1401. items[i] = this._filterFields(items[i], fields);
  1402. }
  1403. }
  1404. }
  1405. // return the results
  1406. if (type == 'DataTable') {
  1407. var columns = this._getColumnNames(data);
  1408. if (id != undefined) {
  1409. // append a single item to the data table
  1410. me._appendRow(data, columns, item);
  1411. }
  1412. else {
  1413. // copy the items to the provided data table
  1414. for (i = 0, len = items.length; i < len; i++) {
  1415. me._appendRow(data, columns, items[i]);
  1416. }
  1417. }
  1418. return data;
  1419. }
  1420. else {
  1421. // return an array
  1422. if (id != undefined) {
  1423. // a single item
  1424. return item;
  1425. }
  1426. else {
  1427. // multiple items
  1428. if (data) {
  1429. // copy the items to the provided array
  1430. for (i = 0, len = items.length; i < len; i++) {
  1431. data.push(items[i]);
  1432. }
  1433. return data;
  1434. }
  1435. else {
  1436. // just return our array
  1437. return items;
  1438. }
  1439. }
  1440. }
  1441. };
  1442. /**
  1443. * Get ids of all items or from a filtered set of items.
  1444. * @param {Object} [options] An Object with options. Available options:
  1445. * {function} [filter] filter items
  1446. * {String | function} [order] Order the items by
  1447. * a field name or custom sort function.
  1448. * @return {Array} ids
  1449. */
  1450. DataSet.prototype.getIds = function (options) {
  1451. var data = this.data,
  1452. filter = options && options.filter,
  1453. order = options && options.order,
  1454. convert = options && options.convert || this.options.convert,
  1455. i,
  1456. len,
  1457. id,
  1458. item,
  1459. items,
  1460. ids = [];
  1461. if (filter) {
  1462. // get filtered items
  1463. if (order) {
  1464. // create ordered list
  1465. items = [];
  1466. for (id in data) {
  1467. if (data.hasOwnProperty(id)) {
  1468. item = this._getItem(id, convert);
  1469. if (filter(item)) {
  1470. items.push(item);
  1471. }
  1472. }
  1473. }
  1474. this._sort(items, order);
  1475. for (i = 0, len = items.length; i < len; i++) {
  1476. ids[i] = items[i][this.fieldId];
  1477. }
  1478. }
  1479. else {
  1480. // create unordered list
  1481. for (id in data) {
  1482. if (data.hasOwnProperty(id)) {
  1483. item = this._getItem(id, convert);
  1484. if (filter(item)) {
  1485. ids.push(item[this.fieldId]);
  1486. }
  1487. }
  1488. }
  1489. }
  1490. }
  1491. else {
  1492. // get all items
  1493. if (order) {
  1494. // create an ordered list
  1495. items = [];
  1496. for (id in data) {
  1497. if (data.hasOwnProperty(id)) {
  1498. items.push(data[id]);
  1499. }
  1500. }
  1501. this._sort(items, order);
  1502. for (i = 0, len = items.length; i < len; i++) {
  1503. ids[i] = items[i][this.fieldId];
  1504. }
  1505. }
  1506. else {
  1507. // create unordered list
  1508. for (id in data) {
  1509. if (data.hasOwnProperty(id)) {
  1510. item = data[id];
  1511. ids.push(item[this.fieldId]);
  1512. }
  1513. }
  1514. }
  1515. }
  1516. return ids;
  1517. };
  1518. /**
  1519. * Execute a callback function for every item in the dataset.
  1520. * The order of the items is not determined.
  1521. * @param {function} callback
  1522. * @param {Object} [options] Available options:
  1523. * {Object.<String, String>} [convert]
  1524. * {String[]} [fields] filter fields
  1525. * {function} [filter] filter items
  1526. * {String | function} [order] Order the items by
  1527. * a field name or custom sort function.
  1528. */
  1529. DataSet.prototype.forEach = function (callback, options) {
  1530. var filter = options && options.filter,
  1531. convert = options && options.convert || this.options.convert,
  1532. data = this.data,
  1533. item,
  1534. id;
  1535. if (options && options.order) {
  1536. // execute forEach on ordered list
  1537. var items = this.get(options);
  1538. for (var i = 0, len = items.length; i < len; i++) {
  1539. item = items[i];
  1540. id = item[this.fieldId];
  1541. callback(item, id);
  1542. }
  1543. }
  1544. else {
  1545. // unordered
  1546. for (id in data) {
  1547. if (data.hasOwnProperty(id)) {
  1548. item = this._getItem(id, convert);
  1549. if (!filter || filter(item)) {
  1550. callback(item, id);
  1551. }
  1552. }
  1553. }
  1554. }
  1555. };
  1556. /**
  1557. * Map every item in the dataset.
  1558. * @param {function} callback
  1559. * @param {Object} [options] Available options:
  1560. * {Object.<String, String>} [convert]
  1561. * {String[]} [fields] filter fields
  1562. * {function} [filter] filter items
  1563. * {String | function} [order] Order the items by
  1564. * a field name or custom sort function.
  1565. * @return {Object[]} mappedItems
  1566. */
  1567. DataSet.prototype.map = function (callback, options) {
  1568. var filter = options && options.filter,
  1569. convert = options && options.convert || this.options.convert,
  1570. mappedItems = [],
  1571. data = this.data,
  1572. item;
  1573. // convert and filter items
  1574. for (var id in data) {
  1575. if (data.hasOwnProperty(id)) {
  1576. item = this._getItem(id, convert);
  1577. if (!filter || filter(item)) {
  1578. mappedItems.push(callback(item, id));
  1579. }
  1580. }
  1581. }
  1582. // order items
  1583. if (options && options.order) {
  1584. this._sort(mappedItems, options.order);
  1585. }
  1586. return mappedItems;
  1587. };
  1588. /**
  1589. * Filter the fields of an item
  1590. * @param {Object} item
  1591. * @param {String[]} fields Field names
  1592. * @return {Object} filteredItem
  1593. * @private
  1594. */
  1595. DataSet.prototype._filterFields = function (item, fields) {
  1596. var filteredItem = {};
  1597. for (var field in item) {
  1598. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  1599. filteredItem[field] = item[field];
  1600. }
  1601. }
  1602. return filteredItem;
  1603. };
  1604. /**
  1605. * Sort the provided array with items
  1606. * @param {Object[]} items
  1607. * @param {String | function} order A field name or custom sort function.
  1608. * @private
  1609. */
  1610. DataSet.prototype._sort = function (items, order) {
  1611. if (util.isString(order)) {
  1612. // order by provided field name
  1613. var name = order; // field name
  1614. items.sort(function (a, b) {
  1615. var av = a[name];
  1616. var bv = b[name];
  1617. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  1618. });
  1619. }
  1620. else if (typeof order === 'function') {
  1621. // order by sort function
  1622. items.sort(order);
  1623. }
  1624. // TODO: extend order by an Object {field:String, direction:String}
  1625. // where direction can be 'asc' or 'desc'
  1626. else {
  1627. throw new TypeError('Order must be a function or a string');
  1628. }
  1629. };
  1630. /**
  1631. * Remove an object by pointer or by id
  1632. * @param {String | Number | Object | Array} id Object or id, or an array with
  1633. * objects or ids to be removed
  1634. * @param {String} [senderId] Optional sender id
  1635. * @return {Array} removedIds
  1636. */
  1637. DataSet.prototype.remove = function (id, senderId) {
  1638. var removedIds = [],
  1639. i, len, removedId;
  1640. if (id instanceof Array) {
  1641. for (i = 0, len = id.length; i < len; i++) {
  1642. removedId = this._remove(id[i]);
  1643. if (removedId != null) {
  1644. removedIds.push(removedId);
  1645. }
  1646. }
  1647. }
  1648. else {
  1649. removedId = this._remove(id);
  1650. if (removedId != null) {
  1651. removedIds.push(removedId);
  1652. }
  1653. }
  1654. if (removedIds.length) {
  1655. this._trigger('remove', {items: removedIds}, senderId);
  1656. }
  1657. return removedIds;
  1658. };
  1659. /**
  1660. * Remove an item by its id
  1661. * @param {Number | String | Object} id id or item
  1662. * @returns {Number | String | null} id
  1663. * @private
  1664. */
  1665. DataSet.prototype._remove = function (id) {
  1666. if (util.isNumber(id) || util.isString(id)) {
  1667. if (this.data[id]) {
  1668. delete this.data[id];
  1669. delete this.internalIds[id];
  1670. return id;
  1671. }
  1672. }
  1673. else if (id instanceof Object) {
  1674. var itemId = id[this.fieldId];
  1675. if (itemId && this.data[itemId]) {
  1676. delete this.data[itemId];
  1677. delete this.internalIds[itemId];
  1678. return itemId;
  1679. }
  1680. }
  1681. return null;
  1682. };
  1683. /**
  1684. * Clear the data
  1685. * @param {String} [senderId] Optional sender id
  1686. * @return {Array} removedIds The ids of all removed items
  1687. */
  1688. DataSet.prototype.clear = function (senderId) {
  1689. var ids = Object.keys(this.data);
  1690. this.data = {};
  1691. this.internalIds = {};
  1692. this._trigger('remove', {items: ids}, senderId);
  1693. return ids;
  1694. };
  1695. /**
  1696. * Find the item with maximum value of a specified field
  1697. * @param {String} field
  1698. * @return {Object | null} item Item containing max value, or null if no items
  1699. */
  1700. DataSet.prototype.max = function (field) {
  1701. var data = this.data,
  1702. max = null,
  1703. maxField = null;
  1704. for (var id in data) {
  1705. if (data.hasOwnProperty(id)) {
  1706. var item = data[id];
  1707. var itemField = item[field];
  1708. if (itemField != null && (!max || itemField > maxField)) {
  1709. max = item;
  1710. maxField = itemField;
  1711. }
  1712. }
  1713. }
  1714. return max;
  1715. };
  1716. /**
  1717. * Find the item with minimum value of a specified field
  1718. * @param {String} field
  1719. * @return {Object | null} item Item containing max value, or null if no items
  1720. */
  1721. DataSet.prototype.min = function (field) {
  1722. var data = this.data,
  1723. min = null,
  1724. minField = null;
  1725. for (var id in data) {
  1726. if (data.hasOwnProperty(id)) {
  1727. var item = data[id];
  1728. var itemField = item[field];
  1729. if (itemField != null && (!min || itemField < minField)) {
  1730. min = item;
  1731. minField = itemField;
  1732. }
  1733. }
  1734. }
  1735. return min;
  1736. };
  1737. /**
  1738. * Find all distinct values of a specified field
  1739. * @param {String} field
  1740. * @return {Array} values Array containing all distinct values. If the data
  1741. * items do not contain the specified field, an array
  1742. * containing a single value undefined is returned.
  1743. * The returned array is unordered.
  1744. */
  1745. DataSet.prototype.distinct = function (field) {
  1746. var data = this.data,
  1747. values = [],
  1748. fieldType = this.options.convert[field],
  1749. count = 0;
  1750. for (var prop in data) {
  1751. if (data.hasOwnProperty(prop)) {
  1752. var item = data[prop];
  1753. var value = util.convert(item[field], fieldType);
  1754. var exists = false;
  1755. for (var i = 0; i < count; i++) {
  1756. if (values[i] == value) {
  1757. exists = true;
  1758. break;
  1759. }
  1760. }
  1761. if (!exists) {
  1762. values[count] = value;
  1763. count++;
  1764. }
  1765. }
  1766. }
  1767. return values;
  1768. };
  1769. /**
  1770. * Add a single item. Will fail when an item with the same id already exists.
  1771. * @param {Object} item
  1772. * @return {String} id
  1773. * @private
  1774. */
  1775. DataSet.prototype._addItem = function (item) {
  1776. var id = item[this.fieldId];
  1777. if (id != undefined) {
  1778. // check whether this id is already taken
  1779. if (this.data[id]) {
  1780. // item already exists
  1781. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  1782. }
  1783. }
  1784. else {
  1785. // generate an id
  1786. id = util.randomUUID();
  1787. item[this.fieldId] = id;
  1788. this.internalIds[id] = item;
  1789. }
  1790. var d = {};
  1791. for (var field in item) {
  1792. if (item.hasOwnProperty(field)) {
  1793. var fieldType = this.convert[field]; // type may be undefined
  1794. d[field] = util.convert(item[field], fieldType);
  1795. }
  1796. }
  1797. this.data[id] = d;
  1798. return id;
  1799. };
  1800. /**
  1801. * Get an item. Fields can be converted to a specific type
  1802. * @param {String} id
  1803. * @param {Object.<String, String>} [convert] field types to convert
  1804. * @return {Object | null} item
  1805. * @private
  1806. */
  1807. DataSet.prototype._getItem = function (id, convert) {
  1808. var field, value;
  1809. // get the item from the dataset
  1810. var raw = this.data[id];
  1811. if (!raw) {
  1812. return null;
  1813. }
  1814. // convert the items field types
  1815. var converted = {},
  1816. fieldId = this.fieldId,
  1817. internalIds = this.internalIds;
  1818. if (convert) {
  1819. for (field in raw) {
  1820. if (raw.hasOwnProperty(field)) {
  1821. value = raw[field];
  1822. // output all fields, except internal ids
  1823. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1824. converted[field] = util.convert(value, convert[field]);
  1825. }
  1826. }
  1827. }
  1828. }
  1829. else {
  1830. // no field types specified, no converting needed
  1831. for (field in raw) {
  1832. if (raw.hasOwnProperty(field)) {
  1833. value = raw[field];
  1834. // output all fields, except internal ids
  1835. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  1836. converted[field] = value;
  1837. }
  1838. }
  1839. }
  1840. }
  1841. return converted;
  1842. };
  1843. /**
  1844. * Update a single item: merge with existing item.
  1845. * Will fail when the item has no id, or when there does not exist an item
  1846. * with the same id.
  1847. * @param {Object} item
  1848. * @return {String} id
  1849. * @private
  1850. */
  1851. DataSet.prototype._updateItem = function (item) {
  1852. var id = item[this.fieldId];
  1853. if (id == undefined) {
  1854. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  1855. }
  1856. var d = this.data[id];
  1857. if (!d) {
  1858. // item doesn't exist
  1859. throw new Error('Cannot update item: no item with id ' + id + ' found');
  1860. }
  1861. // merge with current item
  1862. for (var field in item) {
  1863. if (item.hasOwnProperty(field)) {
  1864. var fieldType = this.convert[field]; // type may be undefined
  1865. d[field] = util.convert(item[field], fieldType);
  1866. }
  1867. }
  1868. return id;
  1869. };
  1870. /**
  1871. * check if an id is an internal or external id
  1872. * @param id
  1873. * @returns {boolean}
  1874. * @private
  1875. */
  1876. DataSet.prototype.isInternalId = function(id) {
  1877. return (id in this.internalIds);
  1878. };
  1879. /**
  1880. * Get an array with the column names of a Google DataTable
  1881. * @param {DataTable} dataTable
  1882. * @return {String[]} columnNames
  1883. * @private
  1884. */
  1885. DataSet.prototype._getColumnNames = function (dataTable) {
  1886. var columns = [];
  1887. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  1888. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  1889. }
  1890. return columns;
  1891. };
  1892. /**
  1893. * Append an item as a row to the dataTable
  1894. * @param dataTable
  1895. * @param columns
  1896. * @param item
  1897. * @private
  1898. */
  1899. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  1900. var row = dataTable.addRow();
  1901. for (var col = 0, cols = columns.length; col < cols; col++) {
  1902. var field = columns[col];
  1903. dataTable.setValue(row, col, item[field]);
  1904. }
  1905. };
  1906. /**
  1907. * DataView
  1908. *
  1909. * a dataview offers a filtered view on a dataset or an other dataview.
  1910. *
  1911. * @param {DataSet | DataView} data
  1912. * @param {Object} [options] Available options: see method get
  1913. *
  1914. * @constructor DataView
  1915. */
  1916. function DataView (data, options) {
  1917. this.id = util.randomUUID();
  1918. this.data = null;
  1919. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  1920. this.options = options || {};
  1921. this.fieldId = 'id'; // name of the field containing id
  1922. this.subscribers = {}; // event subscribers
  1923. var me = this;
  1924. this.listener = function () {
  1925. me._onEvent.apply(me, arguments);
  1926. };
  1927. this.setData(data);
  1928. }
  1929. // TODO: implement a function .config() to dynamically update things like configured filter
  1930. // and trigger changes accordingly
  1931. /**
  1932. * Set a data source for the view
  1933. * @param {DataSet | DataView} data
  1934. */
  1935. DataView.prototype.setData = function (data) {
  1936. var ids, dataItems, i, len;
  1937. if (this.data) {
  1938. // unsubscribe from current dataset
  1939. if (this.data.unsubscribe) {
  1940. this.data.unsubscribe('*', this.listener);
  1941. }
  1942. // trigger a remove of all items in memory
  1943. ids = [];
  1944. for (var id in this.ids) {
  1945. if (this.ids.hasOwnProperty(id)) {
  1946. ids.push(id);
  1947. }
  1948. }
  1949. this.ids = {};
  1950. this._trigger('remove', {items: ids});
  1951. }
  1952. this.data = data;
  1953. if (this.data) {
  1954. // update fieldId
  1955. this.fieldId = this.options.fieldId ||
  1956. (this.data && this.data.options && this.data.options.fieldId) ||
  1957. 'id';
  1958. // trigger an add of all added items
  1959. ids = this.data.getIds({filter: this.options && this.options.filter});
  1960. for (i = 0, len = ids.length; i < len; i++) {
  1961. id = ids[i];
  1962. this.ids[id] = true;
  1963. }
  1964. this._trigger('add', {items: ids});
  1965. // subscribe to new dataset
  1966. if (this.data.subscribe) {
  1967. this.data.subscribe('*', this.listener);
  1968. }
  1969. }
  1970. };
  1971. /**
  1972. * Get data from the data view
  1973. *
  1974. * Usage:
  1975. *
  1976. * get()
  1977. * get(options: Object)
  1978. * get(options: Object, data: Array | DataTable)
  1979. *
  1980. * get(id: Number)
  1981. * get(id: Number, options: Object)
  1982. * get(id: Number, options: Object, data: Array | DataTable)
  1983. *
  1984. * get(ids: Number[])
  1985. * get(ids: Number[], options: Object)
  1986. * get(ids: Number[], options: Object, data: Array | DataTable)
  1987. *
  1988. * Where:
  1989. *
  1990. * {Number | String} id The id of an item
  1991. * {Number[] | String{}} ids An array with ids of items
  1992. * {Object} options An Object with options. Available options:
  1993. * {String} [type] Type of data to be returned. Can
  1994. * be 'DataTable' or 'Array' (default)
  1995. * {Object.<String, String>} [convert]
  1996. * {String[]} [fields] field names to be returned
  1997. * {function} [filter] filter items
  1998. * {String | function} [order] Order the items by
  1999. * a field name or custom sort function.
  2000. * {Array | DataTable} [data] If provided, items will be appended to this
  2001. * array or table. Required in case of Google
  2002. * DataTable.
  2003. * @param args
  2004. */
  2005. DataView.prototype.get = function (args) {
  2006. var me = this;
  2007. // parse the arguments
  2008. var ids, options, data;
  2009. var firstType = util.getType(arguments[0]);
  2010. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  2011. // get(id(s) [, options] [, data])
  2012. ids = arguments[0]; // can be a single id or an array with ids
  2013. options = arguments[1];
  2014. data = arguments[2];
  2015. }
  2016. else {
  2017. // get([, options] [, data])
  2018. options = arguments[0];
  2019. data = arguments[1];
  2020. }
  2021. // extend the options with the default options and provided options
  2022. var viewOptions = util.extend({}, this.options, options);
  2023. // create a combined filter method when needed
  2024. if (this.options.filter && options && options.filter) {
  2025. viewOptions.filter = function (item) {
  2026. return me.options.filter(item) && options.filter(item);
  2027. }
  2028. }
  2029. // build up the call to the linked data set
  2030. var getArguments = [];
  2031. if (ids != undefined) {
  2032. getArguments.push(ids);
  2033. }
  2034. getArguments.push(viewOptions);
  2035. getArguments.push(data);
  2036. return this.data && this.data.get.apply(this.data, getArguments);
  2037. };
  2038. /**
  2039. * Get ids of all items or from a filtered set of items.
  2040. * @param {Object} [options] An Object with options. Available options:
  2041. * {function} [filter] filter items
  2042. * {String | function} [order] Order the items by
  2043. * a field name or custom sort function.
  2044. * @return {Array} ids
  2045. */
  2046. DataView.prototype.getIds = function (options) {
  2047. var ids;
  2048. if (this.data) {
  2049. var defaultFilter = this.options.filter;
  2050. var filter;
  2051. if (options && options.filter) {
  2052. if (defaultFilter) {
  2053. filter = function (item) {
  2054. return defaultFilter(item) && options.filter(item);
  2055. }
  2056. }
  2057. else {
  2058. filter = options.filter;
  2059. }
  2060. }
  2061. else {
  2062. filter = defaultFilter;
  2063. }
  2064. ids = this.data.getIds({
  2065. filter: filter,
  2066. order: options && options.order
  2067. });
  2068. }
  2069. else {
  2070. ids = [];
  2071. }
  2072. return ids;
  2073. };
  2074. /**
  2075. * Event listener. Will propagate all events from the connected data set to
  2076. * the subscribers of the DataView, but will filter the items and only trigger
  2077. * when there are changes in the filtered data set.
  2078. * @param {String} event
  2079. * @param {Object | null} params
  2080. * @param {String} senderId
  2081. * @private
  2082. */
  2083. DataView.prototype._onEvent = function (event, params, senderId) {
  2084. var i, len, id, item,
  2085. ids = params && params.items,
  2086. data = this.data,
  2087. added = [],
  2088. updated = [],
  2089. removed = [];
  2090. if (ids && data) {
  2091. switch (event) {
  2092. case 'add':
  2093. // filter the ids of the added items
  2094. for (i = 0, len = ids.length; i < len; i++) {
  2095. id = ids[i];
  2096. item = this.get(id);
  2097. if (item) {
  2098. this.ids[id] = true;
  2099. added.push(id);
  2100. }
  2101. }
  2102. break;
  2103. case 'update':
  2104. // determine the event from the views viewpoint: an updated
  2105. // item can be added, updated, or removed from this view.
  2106. for (i = 0, len = ids.length; i < len; i++) {
  2107. id = ids[i];
  2108. item = this.get(id);
  2109. if (item) {
  2110. if (this.ids[id]) {
  2111. updated.push(id);
  2112. }
  2113. else {
  2114. this.ids[id] = true;
  2115. added.push(id);
  2116. }
  2117. }
  2118. else {
  2119. if (this.ids[id]) {
  2120. delete this.ids[id];
  2121. removed.push(id);
  2122. }
  2123. else {
  2124. // nothing interesting for me :-(
  2125. }
  2126. }
  2127. }
  2128. break;
  2129. case 'remove':
  2130. // filter the ids of the removed items
  2131. for (i = 0, len = ids.length; i < len; i++) {
  2132. id = ids[i];
  2133. if (this.ids[id]) {
  2134. delete this.ids[id];
  2135. removed.push(id);
  2136. }
  2137. }
  2138. break;
  2139. }
  2140. if (added.length) {
  2141. this._trigger('add', {items: added}, senderId);
  2142. }
  2143. if (updated.length) {
  2144. this._trigger('update', {items: updated}, senderId);
  2145. }
  2146. if (removed.length) {
  2147. this._trigger('remove', {items: removed}, senderId);
  2148. }
  2149. }
  2150. };
  2151. // copy subscription functionality from DataSet
  2152. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  2153. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  2154. DataView.prototype._trigger = DataSet.prototype._trigger;
  2155. /**
  2156. * @constructor TimeStep
  2157. * The class TimeStep is an iterator for dates. You provide a start date and an
  2158. * end date. The class itself determines the best scale (step size) based on the
  2159. * provided start Date, end Date, and minimumStep.
  2160. *
  2161. * If minimumStep is provided, the step size is chosen as close as possible
  2162. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2163. * provided, the scale is set to 1 DAY.
  2164. * The minimumStep should correspond with the onscreen size of about 6 characters
  2165. *
  2166. * Alternatively, you can set a scale by hand.
  2167. * After creation, you can initialize the class by executing first(). Then you
  2168. * can iterate from the start date to the end date via next(). You can check if
  2169. * the end date is reached with the function hasNext(). After each step, you can
  2170. * retrieve the current date via getCurrent().
  2171. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  2172. * days, to years.
  2173. *
  2174. * Version: 1.2
  2175. *
  2176. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  2177. * or new Date(2010, 9, 21, 23, 45, 00)
  2178. * @param {Date} [end] The end date
  2179. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  2180. */
  2181. TimeStep = function(start, end, minimumStep) {
  2182. // variables
  2183. this.current = new Date();
  2184. this._start = new Date();
  2185. this._end = new Date();
  2186. this.autoScale = true;
  2187. this.scale = TimeStep.SCALE.DAY;
  2188. this.step = 1;
  2189. // initialize the range
  2190. this.setRange(start, end, minimumStep);
  2191. };
  2192. /// enum scale
  2193. TimeStep.SCALE = {
  2194. MILLISECOND: 1,
  2195. SECOND: 2,
  2196. MINUTE: 3,
  2197. HOUR: 4,
  2198. DAY: 5,
  2199. WEEKDAY: 6,
  2200. MONTH: 7,
  2201. YEAR: 8
  2202. };
  2203. /**
  2204. * Set a new range
  2205. * If minimumStep is provided, the step size is chosen as close as possible
  2206. * to the minimumStep but larger than minimumStep. If minimumStep is not
  2207. * provided, the scale is set to 1 DAY.
  2208. * The minimumStep should correspond with the onscreen size of about 6 characters
  2209. * @param {Date} [start] The start date and time.
  2210. * @param {Date} [end] The end date and time.
  2211. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  2212. */
  2213. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  2214. if (!(start instanceof Date) || !(end instanceof Date)) {
  2215. throw "No legal start or end date in method setRange";
  2216. }
  2217. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  2218. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  2219. if (this.autoScale) {
  2220. this.setMinimumStep(minimumStep);
  2221. }
  2222. };
  2223. /**
  2224. * Set the range iterator to the start date.
  2225. */
  2226. TimeStep.prototype.first = function() {
  2227. this.current = new Date(this._start.valueOf());
  2228. this.roundToMinor();
  2229. };
  2230. /**
  2231. * Round the current date to the first minor date value
  2232. * This must be executed once when the current date is set to start Date
  2233. */
  2234. TimeStep.prototype.roundToMinor = function() {
  2235. // round to floor
  2236. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  2237. //noinspection FallthroughInSwitchStatementJS
  2238. switch (this.scale) {
  2239. case TimeStep.SCALE.YEAR:
  2240. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  2241. this.current.setMonth(0);
  2242. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  2243. case TimeStep.SCALE.DAY: // intentional fall through
  2244. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  2245. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  2246. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  2247. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  2248. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  2249. }
  2250. if (this.step != 1) {
  2251. // round down to the first minor value that is a multiple of the current step size
  2252. switch (this.scale) {
  2253. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  2254. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  2255. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  2256. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  2257. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2258. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  2259. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  2260. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  2261. default: break;
  2262. }
  2263. }
  2264. };
  2265. /**
  2266. * Check if the there is a next step
  2267. * @return {boolean} true if the current date has not passed the end date
  2268. */
  2269. TimeStep.prototype.hasNext = function () {
  2270. return (this.current.valueOf() <= this._end.valueOf());
  2271. };
  2272. /**
  2273. * Do the next step
  2274. */
  2275. TimeStep.prototype.next = function() {
  2276. var prev = this.current.valueOf();
  2277. // Two cases, needed to prevent issues with switching daylight savings
  2278. // (end of March and end of October)
  2279. if (this.current.getMonth() < 6) {
  2280. switch (this.scale) {
  2281. case TimeStep.SCALE.MILLISECOND:
  2282. this.current = new Date(this.current.valueOf() + this.step); break;
  2283. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  2284. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  2285. case TimeStep.SCALE.HOUR:
  2286. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  2287. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  2288. var h = this.current.getHours();
  2289. this.current.setHours(h - (h % this.step));
  2290. break;
  2291. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2292. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2293. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2294. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2295. default: break;
  2296. }
  2297. }
  2298. else {
  2299. switch (this.scale) {
  2300. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  2301. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  2302. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  2303. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  2304. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2305. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  2306. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  2307. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  2308. default: break;
  2309. }
  2310. }
  2311. if (this.step != 1) {
  2312. // round down to the correct major value
  2313. switch (this.scale) {
  2314. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  2315. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  2316. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  2317. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  2318. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2319. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  2320. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  2321. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  2322. default: break;
  2323. }
  2324. }
  2325. // safety mechanism: if current time is still unchanged, move to the end
  2326. if (this.current.valueOf() == prev) {
  2327. this.current = new Date(this._end.valueOf());
  2328. }
  2329. };
  2330. /**
  2331. * Get the current datetime
  2332. * @return {Date} current The current date
  2333. */
  2334. TimeStep.prototype.getCurrent = function() {
  2335. return this.current;
  2336. };
  2337. /**
  2338. * Set a custom scale. Autoscaling will be disabled.
  2339. * For example setScale(SCALE.MINUTES, 5) will result
  2340. * in minor steps of 5 minutes, and major steps of an hour.
  2341. *
  2342. * @param {TimeStep.SCALE} newScale
  2343. * A scale. Choose from SCALE.MILLISECOND,
  2344. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  2345. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  2346. * SCALE.YEAR.
  2347. * @param {Number} newStep A step size, by default 1. Choose for
  2348. * example 1, 2, 5, or 10.
  2349. */
  2350. TimeStep.prototype.setScale = function(newScale, newStep) {
  2351. this.scale = newScale;
  2352. if (newStep > 0) {
  2353. this.step = newStep;
  2354. }
  2355. this.autoScale = false;
  2356. };
  2357. /**
  2358. * Enable or disable autoscaling
  2359. * @param {boolean} enable If true, autoascaling is set true
  2360. */
  2361. TimeStep.prototype.setAutoScale = function (enable) {
  2362. this.autoScale = enable;
  2363. };
  2364. /**
  2365. * Automatically determine the scale that bests fits the provided minimum step
  2366. * @param {Number} [minimumStep] The minimum step size in milliseconds
  2367. */
  2368. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  2369. if (minimumStep == undefined) {
  2370. return;
  2371. }
  2372. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  2373. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  2374. var stepDay = (1000 * 60 * 60 * 24);
  2375. var stepHour = (1000 * 60 * 60);
  2376. var stepMinute = (1000 * 60);
  2377. var stepSecond = (1000);
  2378. var stepMillisecond= (1);
  2379. // find the smallest step that is larger than the provided minimumStep
  2380. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  2381. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  2382. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  2383. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  2384. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  2385. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  2386. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  2387. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  2388. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  2389. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  2390. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  2391. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  2392. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  2393. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  2394. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  2395. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  2396. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  2397. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  2398. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  2399. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  2400. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  2401. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  2402. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  2403. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  2404. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  2405. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  2406. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  2407. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  2408. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  2409. };
  2410. /**
  2411. * Snap a date to a rounded value. The snap intervals are dependent on the
  2412. * current scale and step.
  2413. * @param {Date} date the date to be snapped
  2414. */
  2415. TimeStep.prototype.snap = function(date) {
  2416. if (this.scale == TimeStep.SCALE.YEAR) {
  2417. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  2418. date.setFullYear(Math.round(year / this.step) * this.step);
  2419. date.setMonth(0);
  2420. date.setDate(0);
  2421. date.setHours(0);
  2422. date.setMinutes(0);
  2423. date.setSeconds(0);
  2424. date.setMilliseconds(0);
  2425. }
  2426. else if (this.scale == TimeStep.SCALE.MONTH) {
  2427. if (date.getDate() > 15) {
  2428. date.setDate(1);
  2429. date.setMonth(date.getMonth() + 1);
  2430. // important: first set Date to 1, after that change the month.
  2431. }
  2432. else {
  2433. date.setDate(1);
  2434. }
  2435. date.setHours(0);
  2436. date.setMinutes(0);
  2437. date.setSeconds(0);
  2438. date.setMilliseconds(0);
  2439. }
  2440. else if (this.scale == TimeStep.SCALE.DAY ||
  2441. this.scale == TimeStep.SCALE.WEEKDAY) {
  2442. //noinspection FallthroughInSwitchStatementJS
  2443. switch (this.step) {
  2444. case 5:
  2445. case 2:
  2446. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  2447. default:
  2448. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  2449. }
  2450. date.setMinutes(0);
  2451. date.setSeconds(0);
  2452. date.setMilliseconds(0);
  2453. }
  2454. else if (this.scale == TimeStep.SCALE.HOUR) {
  2455. switch (this.step) {
  2456. case 4:
  2457. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  2458. default:
  2459. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  2460. }
  2461. date.setSeconds(0);
  2462. date.setMilliseconds(0);
  2463. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  2464. //noinspection FallthroughInSwitchStatementJS
  2465. switch (this.step) {
  2466. case 15:
  2467. case 10:
  2468. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  2469. date.setSeconds(0);
  2470. break;
  2471. case 5:
  2472. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  2473. default:
  2474. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  2475. }
  2476. date.setMilliseconds(0);
  2477. }
  2478. else if (this.scale == TimeStep.SCALE.SECOND) {
  2479. //noinspection FallthroughInSwitchStatementJS
  2480. switch (this.step) {
  2481. case 15:
  2482. case 10:
  2483. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  2484. date.setMilliseconds(0);
  2485. break;
  2486. case 5:
  2487. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  2488. default:
  2489. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  2490. }
  2491. }
  2492. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  2493. var step = this.step > 5 ? this.step / 2 : 1;
  2494. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  2495. }
  2496. };
  2497. /**
  2498. * Check if the current value is a major value (for example when the step
  2499. * is DAY, a major value is each first day of the MONTH)
  2500. * @return {boolean} true if current date is major, else false.
  2501. */
  2502. TimeStep.prototype.isMajor = function() {
  2503. switch (this.scale) {
  2504. case TimeStep.SCALE.MILLISECOND:
  2505. return (this.current.getMilliseconds() == 0);
  2506. case TimeStep.SCALE.SECOND:
  2507. return (this.current.getSeconds() == 0);
  2508. case TimeStep.SCALE.MINUTE:
  2509. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  2510. // Note: this is no bug. Major label is equal for both minute and hour scale
  2511. case TimeStep.SCALE.HOUR:
  2512. return (this.current.getHours() == 0);
  2513. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  2514. case TimeStep.SCALE.DAY:
  2515. return (this.current.getDate() == 1);
  2516. case TimeStep.SCALE.MONTH:
  2517. return (this.current.getMonth() == 0);
  2518. case TimeStep.SCALE.YEAR:
  2519. return false;
  2520. default:
  2521. return false;
  2522. }
  2523. };
  2524. /**
  2525. * Returns formatted text for the minor axislabel, depending on the current
  2526. * date and the scale. For example when scale is MINUTE, the current time is
  2527. * formatted as "hh:mm".
  2528. * @param {Date} [date] custom date. if not provided, current date is taken
  2529. */
  2530. TimeStep.prototype.getLabelMinor = function(date) {
  2531. if (date == undefined) {
  2532. date = this.current;
  2533. }
  2534. switch (this.scale) {
  2535. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  2536. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  2537. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  2538. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  2539. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  2540. case TimeStep.SCALE.DAY: return moment(date).format('D');
  2541. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  2542. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  2543. default: return '';
  2544. }
  2545. };
  2546. /**
  2547. * Returns formatted text for the major axis label, depending on the current
  2548. * date and the scale. For example when scale is MINUTE, the major scale is
  2549. * hours, and the hour will be formatted as "hh".
  2550. * @param {Date} [date] custom date. if not provided, current date is taken
  2551. */
  2552. TimeStep.prototype.getLabelMajor = function(date) {
  2553. if (date == undefined) {
  2554. date = this.current;
  2555. }
  2556. //noinspection FallthroughInSwitchStatementJS
  2557. switch (this.scale) {
  2558. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  2559. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  2560. case TimeStep.SCALE.MINUTE:
  2561. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  2562. case TimeStep.SCALE.WEEKDAY:
  2563. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  2564. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  2565. case TimeStep.SCALE.YEAR: return '';
  2566. default: return '';
  2567. }
  2568. };
  2569. /**
  2570. * @constructor Stack
  2571. * Stacks items on top of each other.
  2572. * @param {ItemSet} parent
  2573. * @param {Object} [options]
  2574. */
  2575. function Stack (parent, options) {
  2576. this.parent = parent;
  2577. this.options = options || {};
  2578. this.defaultOptions = {
  2579. order: function (a, b) {
  2580. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  2581. // Order: ranges over non-ranges, ranged ordered by width, and
  2582. // lastly ordered by start.
  2583. if (a instanceof ItemRange) {
  2584. if (b instanceof ItemRange) {
  2585. var aInt = (a.data.end - a.data.start);
  2586. var bInt = (b.data.end - b.data.start);
  2587. return (aInt - bInt) || (a.data.start - b.data.start);
  2588. }
  2589. else {
  2590. return -1;
  2591. }
  2592. }
  2593. else {
  2594. if (b instanceof ItemRange) {
  2595. return 1;
  2596. }
  2597. else {
  2598. return (a.data.start - b.data.start);
  2599. }
  2600. }
  2601. },
  2602. margin: {
  2603. item: 10
  2604. }
  2605. };
  2606. this.ordered = []; // ordered items
  2607. }
  2608. /**
  2609. * Set options for the stack
  2610. * @param {Object} options Available options:
  2611. * {ItemSet} parent
  2612. * {Number} margin
  2613. * {function} order Stacking order
  2614. */
  2615. Stack.prototype.setOptions = function setOptions (options) {
  2616. util.extend(this.options, options);
  2617. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  2618. };
  2619. /**
  2620. * Stack the items such that they don't overlap. The items will have a minimal
  2621. * distance equal to options.margin.item.
  2622. */
  2623. Stack.prototype.update = function update() {
  2624. this._order();
  2625. this._stack();
  2626. };
  2627. /**
  2628. * Order the items. The items are ordered by width first, and by left position
  2629. * second.
  2630. * If a custom order function has been provided via the options, then this will
  2631. * be used.
  2632. * @private
  2633. */
  2634. Stack.prototype._order = function _order () {
  2635. var items = this.parent.items;
  2636. if (!items) {
  2637. throw new Error('Cannot stack items: parent does not contain items');
  2638. }
  2639. // TODO: store the sorted items, to have less work later on
  2640. var ordered = [];
  2641. var index = 0;
  2642. // items is a map (no array)
  2643. util.forEach(items, function (item) {
  2644. if (item.visible) {
  2645. ordered[index] = item;
  2646. index++;
  2647. }
  2648. });
  2649. //if a customer stack order function exists, use it.
  2650. var order = this.options.order || this.defaultOptions.order;
  2651. if (!(typeof order === 'function')) {
  2652. throw new Error('Option order must be a function');
  2653. }
  2654. ordered.sort(order);
  2655. this.ordered = ordered;
  2656. };
  2657. /**
  2658. * Adjust vertical positions of the events such that they don't overlap each
  2659. * other.
  2660. * @private
  2661. */
  2662. Stack.prototype._stack = function _stack () {
  2663. var i,
  2664. iMax,
  2665. ordered = this.ordered,
  2666. options = this.options,
  2667. orientation = options.orientation || this.defaultOptions.orientation,
  2668. axisOnTop = (orientation == 'top'),
  2669. margin;
  2670. if (options.margin && options.margin.item !== undefined) {
  2671. margin = options.margin.item;
  2672. }
  2673. else {
  2674. margin = this.defaultOptions.margin.item
  2675. }
  2676. // calculate new, non-overlapping positions
  2677. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  2678. var item = ordered[i];
  2679. var collidingItem = null;
  2680. do {
  2681. // TODO: optimize checking for overlap. when there is a gap without items,
  2682. // you only need to check for items from the next item on, not from zero
  2683. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  2684. if (collidingItem != null) {
  2685. // There is a collision. Reposition the event above the colliding element
  2686. if (axisOnTop) {
  2687. item.top = collidingItem.top + collidingItem.height + margin;
  2688. }
  2689. else {
  2690. item.top = collidingItem.top - item.height - margin;
  2691. }
  2692. }
  2693. } while (collidingItem);
  2694. }
  2695. };
  2696. /**
  2697. * Check if the destiny position of given item overlaps with any
  2698. * of the other items from index itemStart to itemEnd.
  2699. * @param {Array} items Array with items
  2700. * @param {int} itemIndex Number of the item to be checked for overlap
  2701. * @param {int} itemStart First item to be checked.
  2702. * @param {int} itemEnd Last item to be checked.
  2703. * @return {Object | null} colliding item, or undefined when no collisions
  2704. * @param {Number} margin A minimum required margin.
  2705. * If margin is provided, the two items will be
  2706. * marked colliding when they overlap or
  2707. * when the margin between the two is smaller than
  2708. * the requested margin.
  2709. */
  2710. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  2711. itemStart, itemEnd, margin) {
  2712. var collision = this.collision;
  2713. // we loop from end to start, as we suppose that the chance of a
  2714. // collision is larger for items at the end, so check these first.
  2715. var a = items[itemIndex];
  2716. for (var i = itemEnd; i >= itemStart; i--) {
  2717. var b = items[i];
  2718. if (collision(a, b, margin)) {
  2719. if (i != itemIndex) {
  2720. return b;
  2721. }
  2722. }
  2723. }
  2724. return null;
  2725. };
  2726. /**
  2727. * Test if the two provided items collide
  2728. * The items must have parameters left, width, top, and height.
  2729. * @param {Component} a The first item
  2730. * @param {Component} b The second item
  2731. * @param {Number} margin A minimum required margin.
  2732. * If margin is provided, the two items will be
  2733. * marked colliding when they overlap or
  2734. * when the margin between the two is smaller than
  2735. * the requested margin.
  2736. * @return {boolean} true if a and b collide, else false
  2737. */
  2738. Stack.prototype.collision = function collision (a, b, margin) {
  2739. return ((a.left - margin) < (b.left + b.getWidth()) &&
  2740. (a.left + a.getWidth() + margin) > b.left &&
  2741. (a.top - margin) < (b.top + b.height) &&
  2742. (a.top + a.height + margin) > b.top);
  2743. };
  2744. /**
  2745. * @constructor Range
  2746. * A Range controls a numeric range with a start and end value.
  2747. * The Range adjusts the range based on mouse events or programmatic changes,
  2748. * and triggers events when the range is changing or has been changed.
  2749. * @param {Object} [options] See description at Range.setOptions
  2750. * @extends Controller
  2751. */
  2752. function Range(options) {
  2753. this.id = util.randomUUID();
  2754. this.start = null; // Number
  2755. this.end = null; // Number
  2756. this.options = options || {};
  2757. this.setOptions(options);
  2758. }
  2759. /**
  2760. * Set options for the range controller
  2761. * @param {Object} options Available options:
  2762. * {Number} min Minimum value for start
  2763. * {Number} max Maximum value for end
  2764. * {Number} zoomMin Set a minimum value for
  2765. * (end - start).
  2766. * {Number} zoomMax Set a maximum value for
  2767. * (end - start).
  2768. */
  2769. Range.prototype.setOptions = function (options) {
  2770. util.extend(this.options, options);
  2771. // re-apply range with new limitations
  2772. if (this.start !== null && this.end !== null) {
  2773. this.setRange(this.start, this.end);
  2774. }
  2775. };
  2776. /**
  2777. * Test whether direction has a valid value
  2778. * @param {String} direction 'horizontal' or 'vertical'
  2779. */
  2780. function validateDirection (direction) {
  2781. if (direction != 'horizontal' && direction != 'vertical') {
  2782. throw new TypeError('Unknown direction "' + direction + '". ' +
  2783. 'Choose "horizontal" or "vertical".');
  2784. }
  2785. }
  2786. /**
  2787. * Add listeners for mouse and touch events to the component
  2788. * @param {Component} component
  2789. * @param {String} event Available events: 'move', 'zoom'
  2790. * @param {String} direction Available directions: 'horizontal', 'vertical'
  2791. */
  2792. Range.prototype.subscribe = function (component, event, direction) {
  2793. var me = this;
  2794. if (event == 'move') {
  2795. // drag start listener
  2796. component.on('dragstart', function (event) {
  2797. me._onDragStart(event, component);
  2798. });
  2799. // drag listener
  2800. component.on('drag', function (event) {
  2801. me._onDrag(event, component, direction);
  2802. });
  2803. // drag end listener
  2804. component.on('dragend', function (event) {
  2805. me._onDragEnd(event, component);
  2806. });
  2807. }
  2808. else if (event == 'zoom') {
  2809. // mouse wheel
  2810. function mousewheel (event) {
  2811. me._onMouseWheel(event, component, direction);
  2812. }
  2813. component.on('mousewheel', mousewheel);
  2814. component.on('DOMMouseScroll', mousewheel); // For FF
  2815. // pinch
  2816. component.on('touch', function (event) {
  2817. me._onTouch();
  2818. });
  2819. component.on('pinch', function (event) {
  2820. me._onPinch(event, component, direction);
  2821. });
  2822. }
  2823. else {
  2824. throw new TypeError('Unknown event "' + event + '". ' +
  2825. 'Choose "move" or "zoom".');
  2826. }
  2827. };
  2828. /**
  2829. * Event handler
  2830. * @param {String} event name of the event, for example 'click', 'mousemove'
  2831. * @param {function} callback callback handler, invoked with the raw HTML Event
  2832. * as parameter.
  2833. */
  2834. Range.prototype.on = function (event, callback) {
  2835. events.addListener(this, event, callback);
  2836. };
  2837. /**
  2838. * Trigger an event
  2839. * @param {String} event name of the event, available events: 'rangechange',
  2840. * 'rangechanged'
  2841. * @private
  2842. */
  2843. Range.prototype._trigger = function (event) {
  2844. events.trigger(this, event, {
  2845. start: this.start,
  2846. end: this.end
  2847. });
  2848. };
  2849. /**
  2850. * Set a new start and end range
  2851. * @param {Number} [start]
  2852. * @param {Number} [end]
  2853. */
  2854. Range.prototype.setRange = function(start, end) {
  2855. var changed = this._applyRange(start, end);
  2856. if (changed) {
  2857. this._trigger('rangechange');
  2858. this._trigger('rangechanged');
  2859. }
  2860. };
  2861. /**
  2862. * Set a new start and end range. This method is the same as setRange, but
  2863. * does not trigger a range change and range changed event, and it returns
  2864. * true when the range is changed
  2865. * @param {Number} [start]
  2866. * @param {Number} [end]
  2867. * @return {Boolean} changed
  2868. * @private
  2869. */
  2870. Range.prototype._applyRange = function(start, end) {
  2871. var newStart = (start != null) ? util.convert(start, 'Number') : this.start,
  2872. newEnd = (end != null) ? util.convert(end, 'Number') : this.end,
  2873. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  2874. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  2875. diff;
  2876. // check for valid number
  2877. if (isNaN(newStart) || newStart === null) {
  2878. throw new Error('Invalid start "' + start + '"');
  2879. }
  2880. if (isNaN(newEnd) || newEnd === null) {
  2881. throw new Error('Invalid end "' + end + '"');
  2882. }
  2883. // prevent start < end
  2884. if (newEnd < newStart) {
  2885. newEnd = newStart;
  2886. }
  2887. // prevent start < min
  2888. if (min !== null) {
  2889. if (newStart < min) {
  2890. diff = (min - newStart);
  2891. newStart += diff;
  2892. newEnd += diff;
  2893. // prevent end > max
  2894. if (max != null) {
  2895. if (newEnd > max) {
  2896. newEnd = max;
  2897. }
  2898. }
  2899. }
  2900. }
  2901. // prevent end > max
  2902. if (max !== null) {
  2903. if (newEnd > max) {
  2904. diff = (newEnd - max);
  2905. newStart -= diff;
  2906. newEnd -= diff;
  2907. // prevent start < min
  2908. if (min != null) {
  2909. if (newStart < min) {
  2910. newStart = min;
  2911. }
  2912. }
  2913. }
  2914. }
  2915. // prevent (end-start) < zoomMin
  2916. if (this.options.zoomMin !== null) {
  2917. var zoomMin = parseFloat(this.options.zoomMin);
  2918. if (zoomMin < 0) {
  2919. zoomMin = 0;
  2920. }
  2921. if ((newEnd - newStart) < zoomMin) {
  2922. if ((this.end - this.start) === zoomMin) {
  2923. // ignore this action, we are already zoomed to the minimum
  2924. newStart = this.start;
  2925. newEnd = this.end;
  2926. }
  2927. else {
  2928. // zoom to the minimum
  2929. diff = (zoomMin - (newEnd - newStart));
  2930. newStart -= diff / 2;
  2931. newEnd += diff / 2;
  2932. }
  2933. }
  2934. }
  2935. // prevent (end-start) > zoomMax
  2936. if (this.options.zoomMax !== null) {
  2937. var zoomMax = parseFloat(this.options.zoomMax);
  2938. if (zoomMax < 0) {
  2939. zoomMax = 0;
  2940. }
  2941. if ((newEnd - newStart) > zoomMax) {
  2942. if ((this.end - this.start) === zoomMax) {
  2943. // ignore this action, we are already zoomed to the maximum
  2944. newStart = this.start;
  2945. newEnd = this.end;
  2946. }
  2947. else {
  2948. // zoom to the maximum
  2949. diff = ((newEnd - newStart) - zoomMax);
  2950. newStart += diff / 2;
  2951. newEnd -= diff / 2;
  2952. }
  2953. }
  2954. }
  2955. var changed = (this.start != newStart || this.end != newEnd);
  2956. this.start = newStart;
  2957. this.end = newEnd;
  2958. return changed;
  2959. };
  2960. /**
  2961. * Retrieve the current range.
  2962. * @return {Object} An object with start and end properties
  2963. */
  2964. Range.prototype.getRange = function() {
  2965. return {
  2966. start: this.start,
  2967. end: this.end
  2968. };
  2969. };
  2970. /**
  2971. * Calculate the conversion offset and scale for current range, based on
  2972. * the provided width
  2973. * @param {Number} width
  2974. * @returns {{offset: number, scale: number}} conversion
  2975. */
  2976. Range.prototype.conversion = function (width) {
  2977. return Range.conversion(this.start, this.end, width);
  2978. };
  2979. /**
  2980. * Static method to calculate the conversion offset and scale for a range,
  2981. * based on the provided start, end, and width
  2982. * @param {Number} start
  2983. * @param {Number} end
  2984. * @param {Number} width
  2985. * @returns {{offset: number, scale: number}} conversion
  2986. */
  2987. Range.conversion = function (start, end, width) {
  2988. if (width != 0 && (end - start != 0)) {
  2989. return {
  2990. offset: start,
  2991. scale: width / (end - start)
  2992. }
  2993. }
  2994. else {
  2995. return {
  2996. offset: 0,
  2997. scale: 1
  2998. };
  2999. }
  3000. };
  3001. // global (private) object to store drag params
  3002. var touchParams = {};
  3003. /**
  3004. * Start dragging horizontally or vertically
  3005. * @param {Event} event
  3006. * @param {Object} component
  3007. * @private
  3008. */
  3009. Range.prototype._onDragStart = function(event, component) {
  3010. // refuse to drag when we where pinching to prevent the timeline make a jump
  3011. // when releasing the fingers in opposite order from the touch screen
  3012. if (touchParams.pinching) return;
  3013. touchParams.start = this.start;
  3014. touchParams.end = this.end;
  3015. var frame = component.frame;
  3016. if (frame) {
  3017. frame.style.cursor = 'move';
  3018. }
  3019. };
  3020. /**
  3021. * Perform dragging operating.
  3022. * @param {Event} event
  3023. * @param {Component} component
  3024. * @param {String} direction 'horizontal' or 'vertical'
  3025. * @private
  3026. */
  3027. Range.prototype._onDrag = function (event, component, direction) {
  3028. validateDirection(direction);
  3029. // refuse to drag when we where pinching to prevent the timeline make a jump
  3030. // when releasing the fingers in opposite order from the touch screen
  3031. if (touchParams.pinching) return;
  3032. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  3033. interval = (touchParams.end - touchParams.start),
  3034. width = (direction == 'horizontal') ? component.width : component.height,
  3035. diffRange = -delta / width * interval;
  3036. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  3037. // fire a rangechange event
  3038. this._trigger('rangechange');
  3039. };
  3040. /**
  3041. * Stop dragging operating.
  3042. * @param {event} event
  3043. * @param {Component} component
  3044. * @private
  3045. */
  3046. Range.prototype._onDragEnd = 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. if (component.frame) {
  3051. component.frame.style.cursor = 'auto';
  3052. }
  3053. // fire a rangechanged event
  3054. this._trigger('rangechanged');
  3055. };
  3056. /**
  3057. * Event handler for mouse wheel event, used to zoom
  3058. * Code from http://adomas.org/javascript-mouse-wheel/
  3059. * @param {Event} event
  3060. * @param {Component} component
  3061. * @param {String} direction 'horizontal' or 'vertical'
  3062. * @private
  3063. */
  3064. Range.prototype._onMouseWheel = function(event, component, direction) {
  3065. validateDirection(direction);
  3066. // retrieve delta
  3067. var delta = 0;
  3068. if (event.wheelDelta) { /* IE/Opera. */
  3069. delta = event.wheelDelta / 120;
  3070. } else if (event.detail) { /* Mozilla case. */
  3071. // In Mozilla, sign of delta is different than in IE.
  3072. // Also, delta is multiple of 3.
  3073. delta = -event.detail / 3;
  3074. }
  3075. // If delta is nonzero, handle it.
  3076. // Basically, delta is now positive if wheel was scrolled up,
  3077. // and negative, if wheel was scrolled down.
  3078. if (delta) {
  3079. // perform the zoom action. Delta is normally 1 or -1
  3080. // adjust a negative delta such that zooming in with delta 0.1
  3081. // equals zooming out with a delta -0.1
  3082. var scale;
  3083. if (delta < 0) {
  3084. scale = 1 - (delta / 5);
  3085. }
  3086. else {
  3087. scale = 1 / (1 + (delta / 5)) ;
  3088. }
  3089. // calculate center, the date to zoom around
  3090. var gesture = util.fakeGesture(this, event),
  3091. pointer = getPointer(gesture.touches[0], component.frame),
  3092. pointerDate = this._pointerToDate(component, direction, pointer);
  3093. this.zoom(scale, pointerDate);
  3094. }
  3095. // Prevent default actions caused by mouse wheel
  3096. // (else the page and timeline both zoom and scroll)
  3097. util.preventDefault(event);
  3098. };
  3099. /**
  3100. * On start of a touch gesture, initialize scale to 1
  3101. * @private
  3102. */
  3103. Range.prototype._onTouch = function () {
  3104. touchParams.start = this.start;
  3105. touchParams.end = this.end;
  3106. touchParams.pinching = false;
  3107. touchParams.center = null;
  3108. };
  3109. /**
  3110. * Handle pinch event
  3111. * @param {Event} event
  3112. * @param {Component} component
  3113. * @param {String} direction 'horizontal' or 'vertical'
  3114. * @private
  3115. */
  3116. Range.prototype._onPinch = function (event, component, direction) {
  3117. touchParams.pinching = true;
  3118. if (event.gesture.touches.length > 1) {
  3119. if (!touchParams.center) {
  3120. touchParams.center = getPointer(event.gesture.center, component.frame);
  3121. }
  3122. var scale = 1 / event.gesture.scale,
  3123. initDate = this._pointerToDate(component, direction, touchParams.center),
  3124. center = getPointer(event.gesture.center, component.frame),
  3125. date = this._pointerToDate(component, direction, center),
  3126. delta = date - initDate; // TODO: utilize delta
  3127. // calculate new start and end
  3128. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  3129. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  3130. // apply new range
  3131. this.setRange(newStart, newEnd);
  3132. }
  3133. };
  3134. /**
  3135. * Helper function to calculate the center date for zooming
  3136. * @param {Component} component
  3137. * @param {{x: Number, y: Number}} pointer
  3138. * @param {String} direction 'horizontal' or 'vertical'
  3139. * @return {number} date
  3140. * @private
  3141. */
  3142. Range.prototype._pointerToDate = function (component, direction, pointer) {
  3143. var conversion;
  3144. if (direction == 'horizontal') {
  3145. var width = component.width;
  3146. conversion = this.conversion(width);
  3147. return pointer.x / conversion.scale + conversion.offset;
  3148. }
  3149. else {
  3150. var height = component.height;
  3151. conversion = this.conversion(height);
  3152. return pointer.y / conversion.scale + conversion.offset;
  3153. }
  3154. };
  3155. /**
  3156. * Get the pointer location relative to the location of the dom element
  3157. * @param {{pageX: Number, pageY: Number}} touch
  3158. * @param {Element} element HTML DOM element
  3159. * @return {{x: Number, y: Number}} pointer
  3160. * @private
  3161. */
  3162. function getPointer (touch, element) {
  3163. return {
  3164. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  3165. y: touch.pageY - vis.util.getAbsoluteTop(element)
  3166. };
  3167. }
  3168. /**
  3169. * Zoom the range the given scale in or out. Start and end date will
  3170. * be adjusted, and the timeline will be redrawn. You can optionally give a
  3171. * date around which to zoom.
  3172. * For example, try scale = 0.9 or 1.1
  3173. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  3174. * values below 1 will zoom in.
  3175. * @param {Number} [center] Value representing a date around which will
  3176. * be zoomed.
  3177. */
  3178. Range.prototype.zoom = function(scale, center) {
  3179. // if centerDate is not provided, take it half between start Date and end Date
  3180. if (center == null) {
  3181. center = (this.start + this.end) / 2;
  3182. }
  3183. // calculate new start and end
  3184. var newStart = center + (this.start - center) * scale;
  3185. var newEnd = center + (this.end - center) * scale;
  3186. this.setRange(newStart, newEnd);
  3187. };
  3188. /**
  3189. * Move the range with a given delta to the left or right. Start and end
  3190. * value will be adjusted. For example, try delta = 0.1 or -0.1
  3191. * @param {Number} delta Moving amount. Positive value will move right,
  3192. * negative value will move left
  3193. */
  3194. Range.prototype.move = function(delta) {
  3195. // zoom start Date and end Date relative to the centerDate
  3196. var diff = (this.end - this.start);
  3197. // apply new values
  3198. var newStart = this.start + diff * delta;
  3199. var newEnd = this.end + diff * delta;
  3200. // TODO: reckon with min and max range
  3201. this.start = newStart;
  3202. this.end = newEnd;
  3203. };
  3204. /**
  3205. * Move the range to a new center point
  3206. * @param {Number} moveTo New center point of the range
  3207. */
  3208. Range.prototype.moveTo = function(moveTo) {
  3209. var center = (this.start + this.end) / 2;
  3210. var diff = center - moveTo;
  3211. // calculate new start and end
  3212. var newStart = this.start - diff;
  3213. var newEnd = this.end - diff;
  3214. this.setRange(newStart, newEnd);
  3215. };
  3216. /**
  3217. * @constructor Controller
  3218. *
  3219. * A Controller controls the reflows and repaints of all visual components
  3220. */
  3221. function Controller () {
  3222. this.id = util.randomUUID();
  3223. this.components = {};
  3224. this.repaintTimer = undefined;
  3225. this.reflowTimer = undefined;
  3226. }
  3227. /**
  3228. * Add a component to the controller
  3229. * @param {Component} component
  3230. */
  3231. Controller.prototype.add = function add(component) {
  3232. // validate the component
  3233. if (component.id == undefined) {
  3234. throw new Error('Component has no field id');
  3235. }
  3236. if (!(component instanceof Component) && !(component instanceof Controller)) {
  3237. throw new TypeError('Component must be an instance of ' +
  3238. 'prototype Component or Controller');
  3239. }
  3240. // add the component
  3241. component.controller = this;
  3242. this.components[component.id] = component;
  3243. };
  3244. /**
  3245. * Remove a component from the controller
  3246. * @param {Component | String} component
  3247. */
  3248. Controller.prototype.remove = function remove(component) {
  3249. var id;
  3250. for (id in this.components) {
  3251. if (this.components.hasOwnProperty(id)) {
  3252. if (id == component || this.components[id] == component) {
  3253. break;
  3254. }
  3255. }
  3256. }
  3257. if (id) {
  3258. delete this.components[id];
  3259. }
  3260. };
  3261. /**
  3262. * Request a reflow. The controller will schedule a reflow
  3263. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  3264. * is false.
  3265. */
  3266. Controller.prototype.requestReflow = function requestReflow(force) {
  3267. if (force) {
  3268. this.reflow();
  3269. }
  3270. else {
  3271. if (!this.reflowTimer) {
  3272. var me = this;
  3273. this.reflowTimer = setTimeout(function () {
  3274. me.reflowTimer = undefined;
  3275. me.reflow();
  3276. }, 0);
  3277. }
  3278. }
  3279. };
  3280. /**
  3281. * Request a repaint. The controller will schedule a repaint
  3282. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  3283. * is false.
  3284. */
  3285. Controller.prototype.requestRepaint = function requestRepaint(force) {
  3286. if (force) {
  3287. this.repaint();
  3288. }
  3289. else {
  3290. if (!this.repaintTimer) {
  3291. var me = this;
  3292. this.repaintTimer = setTimeout(function () {
  3293. me.repaintTimer = undefined;
  3294. me.repaint();
  3295. }, 0);
  3296. }
  3297. }
  3298. };
  3299. /**
  3300. * Repaint all components
  3301. */
  3302. Controller.prototype.repaint = function repaint() {
  3303. var changed = false;
  3304. // cancel any running repaint request
  3305. if (this.repaintTimer) {
  3306. clearTimeout(this.repaintTimer);
  3307. this.repaintTimer = undefined;
  3308. }
  3309. var done = {};
  3310. function repaint(component, id) {
  3311. if (!(id in done)) {
  3312. // first repaint the components on which this component is dependent
  3313. if (component.depends) {
  3314. component.depends.forEach(function (dep) {
  3315. repaint(dep, dep.id);
  3316. });
  3317. }
  3318. if (component.parent) {
  3319. repaint(component.parent, component.parent.id);
  3320. }
  3321. // repaint the component itself and mark as done
  3322. changed = component.repaint() || changed;
  3323. done[id] = true;
  3324. }
  3325. }
  3326. util.forEach(this.components, repaint);
  3327. // immediately reflow when needed
  3328. if (changed) {
  3329. this.reflow();
  3330. }
  3331. // TODO: limit the number of nested reflows/repaints, prevent loop
  3332. };
  3333. /**
  3334. * Reflow all components
  3335. */
  3336. Controller.prototype.reflow = function reflow() {
  3337. var resized = false;
  3338. // cancel any running repaint request
  3339. if (this.reflowTimer) {
  3340. clearTimeout(this.reflowTimer);
  3341. this.reflowTimer = undefined;
  3342. }
  3343. var done = {};
  3344. function reflow(component, id) {
  3345. if (!(id in done)) {
  3346. // first reflow the components on which this component is dependent
  3347. if (component.depends) {
  3348. component.depends.forEach(function (dep) {
  3349. reflow(dep, dep.id);
  3350. });
  3351. }
  3352. if (component.parent) {
  3353. reflow(component.parent, component.parent.id);
  3354. }
  3355. // reflow the component itself and mark as done
  3356. resized = component.reflow() || resized;
  3357. done[id] = true;
  3358. }
  3359. }
  3360. util.forEach(this.components, reflow);
  3361. // immediately repaint when needed
  3362. if (resized) {
  3363. this.repaint();
  3364. }
  3365. // TODO: limit the number of nested reflows/repaints, prevent loop
  3366. };
  3367. /**
  3368. * Prototype for visual components
  3369. */
  3370. function Component () {
  3371. this.id = null;
  3372. this.parent = null;
  3373. this.depends = null;
  3374. this.controller = null;
  3375. this.options = null;
  3376. this.frame = null; // main DOM element
  3377. this.top = 0;
  3378. this.left = 0;
  3379. this.width = 0;
  3380. this.height = 0;
  3381. }
  3382. /**
  3383. * Set parameters for the frame. Parameters will be merged in current parameter
  3384. * set.
  3385. * @param {Object} options Available parameters:
  3386. * {String | function} [className]
  3387. * {EventBus} [eventBus]
  3388. * {String | Number | function} [left]
  3389. * {String | Number | function} [top]
  3390. * {String | Number | function} [width]
  3391. * {String | Number | function} [height]
  3392. */
  3393. Component.prototype.setOptions = function setOptions(options) {
  3394. if (options) {
  3395. util.extend(this.options, options);
  3396. if (this.controller) {
  3397. this.requestRepaint();
  3398. this.requestReflow();
  3399. }
  3400. }
  3401. };
  3402. /**
  3403. * Get an option value by name
  3404. * The function will first check this.options object, and else will check
  3405. * this.defaultOptions.
  3406. * @param {String} name
  3407. * @return {*} value
  3408. */
  3409. Component.prototype.getOption = function getOption(name) {
  3410. var value;
  3411. if (this.options) {
  3412. value = this.options[name];
  3413. }
  3414. if (value === undefined && this.defaultOptions) {
  3415. value = this.defaultOptions[name];
  3416. }
  3417. return value;
  3418. };
  3419. /**
  3420. * Get the container element of the component, which can be used by a child to
  3421. * add its own widgets. Not all components do have a container for childs, in
  3422. * that case null is returned.
  3423. * @returns {HTMLElement | null} container
  3424. */
  3425. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  3426. Component.prototype.getContainer = function getContainer() {
  3427. // should be implemented by the component
  3428. return null;
  3429. };
  3430. /**
  3431. * Get the frame element of the component, the outer HTML DOM element.
  3432. * @returns {HTMLElement | null} frame
  3433. */
  3434. Component.prototype.getFrame = function getFrame() {
  3435. return this.frame;
  3436. };
  3437. /**
  3438. * Repaint the component
  3439. * @return {Boolean} changed
  3440. */
  3441. Component.prototype.repaint = function repaint() {
  3442. // should be implemented by the component
  3443. return false;
  3444. };
  3445. /**
  3446. * Reflow the component
  3447. * @return {Boolean} resized
  3448. */
  3449. Component.prototype.reflow = function reflow() {
  3450. // should be implemented by the component
  3451. return false;
  3452. };
  3453. /**
  3454. * Hide the component from the DOM
  3455. * @return {Boolean} changed
  3456. */
  3457. Component.prototype.hide = function hide() {
  3458. if (this.frame && this.frame.parentNode) {
  3459. this.frame.parentNode.removeChild(this.frame);
  3460. return true;
  3461. }
  3462. else {
  3463. return false;
  3464. }
  3465. };
  3466. /**
  3467. * Show the component in the DOM (when not already visible).
  3468. * A repaint will be executed when the component is not visible
  3469. * @return {Boolean} changed
  3470. */
  3471. Component.prototype.show = function show() {
  3472. if (!this.frame || !this.frame.parentNode) {
  3473. return this.repaint();
  3474. }
  3475. else {
  3476. return false;
  3477. }
  3478. };
  3479. /**
  3480. * Request a repaint. The controller will schedule a repaint
  3481. */
  3482. Component.prototype.requestRepaint = function requestRepaint() {
  3483. if (this.controller) {
  3484. this.controller.requestRepaint();
  3485. }
  3486. else {
  3487. throw new Error('Cannot request a repaint: no controller configured');
  3488. // TODO: just do a repaint when no parent is configured?
  3489. }
  3490. };
  3491. /**
  3492. * Request a reflow. The controller will schedule a reflow
  3493. */
  3494. Component.prototype.requestReflow = function requestReflow() {
  3495. if (this.controller) {
  3496. this.controller.requestReflow();
  3497. }
  3498. else {
  3499. throw new Error('Cannot request a reflow: no controller configured');
  3500. // TODO: just do a reflow when no parent is configured?
  3501. }
  3502. };
  3503. /**
  3504. * A panel can contain components
  3505. * @param {Component} [parent]
  3506. * @param {Component[]} [depends] Components on which this components depends
  3507. * (except for the parent)
  3508. * @param {Object} [options] Available parameters:
  3509. * {String | Number | function} [left]
  3510. * {String | Number | function} [top]
  3511. * {String | Number | function} [width]
  3512. * {String | Number | function} [height]
  3513. * {String | function} [className]
  3514. * @constructor Panel
  3515. * @extends Component
  3516. */
  3517. function Panel(parent, depends, options) {
  3518. this.id = util.randomUUID();
  3519. this.parent = parent;
  3520. this.depends = depends;
  3521. this.options = options || {};
  3522. }
  3523. Panel.prototype = new Component();
  3524. /**
  3525. * Set options. Will extend the current options.
  3526. * @param {Object} [options] Available parameters:
  3527. * {String | function} [className]
  3528. * {String | Number | function} [left]
  3529. * {String | Number | function} [top]
  3530. * {String | Number | function} [width]
  3531. * {String | Number | function} [height]
  3532. */
  3533. Panel.prototype.setOptions = Component.prototype.setOptions;
  3534. /**
  3535. * Get the container element of the panel, which can be used by a child to
  3536. * add its own widgets.
  3537. * @returns {HTMLElement} container
  3538. */
  3539. Panel.prototype.getContainer = function () {
  3540. return this.frame;
  3541. };
  3542. /**
  3543. * Repaint the component
  3544. * @return {Boolean} changed
  3545. */
  3546. Panel.prototype.repaint = function () {
  3547. var changed = 0,
  3548. update = util.updateProperty,
  3549. asSize = util.option.asSize,
  3550. options = this.options,
  3551. frame = this.frame;
  3552. if (!frame) {
  3553. frame = document.createElement('div');
  3554. frame.className = 'panel';
  3555. var className = options.className;
  3556. if (className) {
  3557. if (typeof className == 'function') {
  3558. util.addClassName(frame, String(className()));
  3559. }
  3560. else {
  3561. util.addClassName(frame, String(className));
  3562. }
  3563. }
  3564. this.frame = frame;
  3565. changed += 1;
  3566. }
  3567. if (!frame.parentNode) {
  3568. if (!this.parent) {
  3569. throw new Error('Cannot repaint panel: no parent attached');
  3570. }
  3571. var parentContainer = this.parent.getContainer();
  3572. if (!parentContainer) {
  3573. throw new Error('Cannot repaint panel: parent has no container element');
  3574. }
  3575. parentContainer.appendChild(frame);
  3576. changed += 1;
  3577. }
  3578. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3579. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3580. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3581. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3582. return (changed > 0);
  3583. };
  3584. /**
  3585. * Reflow the component
  3586. * @return {Boolean} resized
  3587. */
  3588. Panel.prototype.reflow = function () {
  3589. var changed = 0,
  3590. update = util.updateProperty,
  3591. frame = this.frame;
  3592. if (frame) {
  3593. changed += update(this, 'top', frame.offsetTop);
  3594. changed += update(this, 'left', frame.offsetLeft);
  3595. changed += update(this, 'width', frame.offsetWidth);
  3596. changed += update(this, 'height', frame.offsetHeight);
  3597. }
  3598. else {
  3599. changed += 1;
  3600. }
  3601. return (changed > 0);
  3602. };
  3603. /**
  3604. * A root panel can hold components. The root panel must be initialized with
  3605. * a DOM element as container.
  3606. * @param {HTMLElement} container
  3607. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  3608. * @constructor RootPanel
  3609. * @extends Panel
  3610. */
  3611. function RootPanel(container, options) {
  3612. this.id = util.randomUUID();
  3613. this.container = container;
  3614. this.options = options || {};
  3615. this.defaultOptions = {
  3616. autoResize: true
  3617. };
  3618. this.listeners = {}; // event listeners
  3619. }
  3620. RootPanel.prototype = new Panel();
  3621. /**
  3622. * Set options. Will extend the current options.
  3623. * @param {Object} [options] Available parameters:
  3624. * {String | function} [className]
  3625. * {String | Number | function} [left]
  3626. * {String | Number | function} [top]
  3627. * {String | Number | function} [width]
  3628. * {String | Number | function} [height]
  3629. * {Boolean | function} [autoResize]
  3630. */
  3631. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  3632. /**
  3633. * Repaint the component
  3634. * @return {Boolean} changed
  3635. */
  3636. RootPanel.prototype.repaint = function () {
  3637. var changed = 0,
  3638. update = util.updateProperty,
  3639. asSize = util.option.asSize,
  3640. options = this.options,
  3641. frame = this.frame;
  3642. if (!frame) {
  3643. frame = document.createElement('div');
  3644. this.frame = frame;
  3645. changed += 1;
  3646. }
  3647. if (!frame.parentNode) {
  3648. if (!this.container) {
  3649. throw new Error('Cannot repaint root panel: no container attached');
  3650. }
  3651. this.container.appendChild(frame);
  3652. changed += 1;
  3653. }
  3654. frame.className = 'vis timeline rootpanel ' + options.orientation;
  3655. var className = options.className;
  3656. if (className) {
  3657. util.addClassName(frame, util.option.asString(className));
  3658. }
  3659. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  3660. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3661. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3662. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  3663. this._updateEventEmitters();
  3664. this._updateWatch();
  3665. return (changed > 0);
  3666. };
  3667. /**
  3668. * Reflow the component
  3669. * @return {Boolean} resized
  3670. */
  3671. RootPanel.prototype.reflow = function () {
  3672. var changed = 0,
  3673. update = util.updateProperty,
  3674. frame = this.frame;
  3675. if (frame) {
  3676. changed += update(this, 'top', frame.offsetTop);
  3677. changed += update(this, 'left', frame.offsetLeft);
  3678. changed += update(this, 'width', frame.offsetWidth);
  3679. changed += update(this, 'height', frame.offsetHeight);
  3680. }
  3681. else {
  3682. changed += 1;
  3683. }
  3684. return (changed > 0);
  3685. };
  3686. /**
  3687. * Update watching for resize, depending on the current option
  3688. * @private
  3689. */
  3690. RootPanel.prototype._updateWatch = function () {
  3691. var autoResize = this.getOption('autoResize');
  3692. if (autoResize) {
  3693. this._watch();
  3694. }
  3695. else {
  3696. this._unwatch();
  3697. }
  3698. };
  3699. /**
  3700. * Watch for changes in the size of the frame. On resize, the Panel will
  3701. * automatically redraw itself.
  3702. * @private
  3703. */
  3704. RootPanel.prototype._watch = function () {
  3705. var me = this;
  3706. this._unwatch();
  3707. var checkSize = function () {
  3708. var autoResize = me.getOption('autoResize');
  3709. if (!autoResize) {
  3710. // stop watching when the option autoResize is changed to false
  3711. me._unwatch();
  3712. return;
  3713. }
  3714. if (me.frame) {
  3715. // check whether the frame is resized
  3716. if ((me.frame.clientWidth != me.width) ||
  3717. (me.frame.clientHeight != me.height)) {
  3718. me.requestReflow();
  3719. }
  3720. }
  3721. };
  3722. // TODO: automatically cleanup the event listener when the frame is deleted
  3723. util.addEventListener(window, 'resize', checkSize);
  3724. this.watchTimer = setInterval(checkSize, 1000);
  3725. };
  3726. /**
  3727. * Stop watching for a resize of the frame.
  3728. * @private
  3729. */
  3730. RootPanel.prototype._unwatch = function () {
  3731. if (this.watchTimer) {
  3732. clearInterval(this.watchTimer);
  3733. this.watchTimer = undefined;
  3734. }
  3735. // TODO: remove event listener on window.resize
  3736. };
  3737. /**
  3738. * Event handler
  3739. * @param {String} event name of the event, for example 'click', 'mousemove'
  3740. * @param {function} callback callback handler, invoked with the raw HTML Event
  3741. * as parameter.
  3742. */
  3743. RootPanel.prototype.on = function (event, callback) {
  3744. // register the listener at this component
  3745. var arr = this.listeners[event];
  3746. if (!arr) {
  3747. arr = [];
  3748. this.listeners[event] = arr;
  3749. }
  3750. arr.push(callback);
  3751. this._updateEventEmitters();
  3752. };
  3753. /**
  3754. * Update the event listeners for all event emitters
  3755. * @private
  3756. */
  3757. RootPanel.prototype._updateEventEmitters = function () {
  3758. if (this.listeners) {
  3759. var me = this;
  3760. util.forEach(this.listeners, function (listeners, event) {
  3761. if (!me.emitters) {
  3762. me.emitters = {};
  3763. }
  3764. if (!(event in me.emitters)) {
  3765. // create event
  3766. var frame = me.frame;
  3767. if (frame) {
  3768. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  3769. var callback = function(event) {
  3770. listeners.forEach(function (listener) {
  3771. // TODO: filter on event target!
  3772. listener(event);
  3773. });
  3774. };
  3775. me.emitters[event] = callback;
  3776. if (!me.hammer) {
  3777. me.hammer = Hammer(frame, {
  3778. prevent_default: true
  3779. });
  3780. }
  3781. me.hammer.on(event, callback);
  3782. }
  3783. }
  3784. });
  3785. // TODO: be able to delete event listeners
  3786. // TODO: be able to move event listeners to a parent when available
  3787. }
  3788. };
  3789. /**
  3790. * A horizontal time axis
  3791. * @param {Component} parent
  3792. * @param {Component[]} [depends] Components on which this components depends
  3793. * (except for the parent)
  3794. * @param {Object} [options] See TimeAxis.setOptions for the available
  3795. * options.
  3796. * @constructor TimeAxis
  3797. * @extends Component
  3798. */
  3799. function TimeAxis (parent, depends, options) {
  3800. this.id = util.randomUUID();
  3801. this.parent = parent;
  3802. this.depends = depends;
  3803. this.dom = {
  3804. majorLines: [],
  3805. majorTexts: [],
  3806. minorLines: [],
  3807. minorTexts: [],
  3808. redundant: {
  3809. majorLines: [],
  3810. majorTexts: [],
  3811. minorLines: [],
  3812. minorTexts: []
  3813. }
  3814. };
  3815. this.props = {
  3816. range: {
  3817. start: 0,
  3818. end: 0,
  3819. minimumStep: 0
  3820. },
  3821. lineTop: 0
  3822. };
  3823. this.options = options || {};
  3824. this.defaultOptions = {
  3825. orientation: 'bottom', // supported: 'top', 'bottom'
  3826. // TODO: implement timeaxis orientations 'left' and 'right'
  3827. showMinorLabels: true,
  3828. showMajorLabels: true
  3829. };
  3830. this.conversion = null;
  3831. this.range = null;
  3832. }
  3833. TimeAxis.prototype = new Component();
  3834. // TODO: comment options
  3835. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  3836. /**
  3837. * Set a range (start and end)
  3838. * @param {Range | Object} range A Range or an object containing start and end.
  3839. */
  3840. TimeAxis.prototype.setRange = function (range) {
  3841. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  3842. throw new TypeError('Range must be an instance of Range, ' +
  3843. 'or an object containing start and end.');
  3844. }
  3845. this.range = range;
  3846. };
  3847. /**
  3848. * Convert a position on screen (pixels) to a datetime
  3849. * @param {int} x Position on the screen in pixels
  3850. * @return {Date} time The datetime the corresponds with given position x
  3851. */
  3852. TimeAxis.prototype.toTime = function(x) {
  3853. var conversion = this.conversion;
  3854. return new Date(x / conversion.scale + conversion.offset);
  3855. };
  3856. /**
  3857. * Convert a datetime (Date object) into a position on the screen
  3858. * @param {Date} time A date
  3859. * @return {int} x The position on the screen in pixels which corresponds
  3860. * with the given date.
  3861. * @private
  3862. */
  3863. TimeAxis.prototype.toScreen = function(time) {
  3864. var conversion = this.conversion;
  3865. return (time.valueOf() - conversion.offset) * conversion.scale;
  3866. };
  3867. /**
  3868. * Repaint the component
  3869. * @return {Boolean} changed
  3870. */
  3871. TimeAxis.prototype.repaint = function () {
  3872. var changed = 0,
  3873. update = util.updateProperty,
  3874. asSize = util.option.asSize,
  3875. options = this.options,
  3876. orientation = this.getOption('orientation'),
  3877. props = this.props,
  3878. step = this.step;
  3879. var frame = this.frame;
  3880. if (!frame) {
  3881. frame = document.createElement('div');
  3882. this.frame = frame;
  3883. changed += 1;
  3884. }
  3885. frame.className = 'axis';
  3886. // TODO: custom className?
  3887. if (!frame.parentNode) {
  3888. if (!this.parent) {
  3889. throw new Error('Cannot repaint time axis: no parent attached');
  3890. }
  3891. var parentContainer = this.parent.getContainer();
  3892. if (!parentContainer) {
  3893. throw new Error('Cannot repaint time axis: parent has no container element');
  3894. }
  3895. parentContainer.appendChild(frame);
  3896. changed += 1;
  3897. }
  3898. var parent = frame.parentNode;
  3899. if (parent) {
  3900. var beforeChild = frame.nextSibling;
  3901. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  3902. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  3903. (this.props.parentHeight - this.height) + 'px' :
  3904. '0px';
  3905. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  3906. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  3907. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  3908. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  3909. // get characters width and height
  3910. this._repaintMeasureChars();
  3911. if (this.step) {
  3912. this._repaintStart();
  3913. step.first();
  3914. var xFirstMajorLabel = undefined;
  3915. var max = 0;
  3916. while (step.hasNext() && max < 1000) {
  3917. max++;
  3918. var cur = step.getCurrent(),
  3919. x = this.toScreen(cur),
  3920. isMajor = step.isMajor();
  3921. // TODO: lines must have a width, such that we can create css backgrounds
  3922. if (this.getOption('showMinorLabels')) {
  3923. this._repaintMinorText(x, step.getLabelMinor());
  3924. }
  3925. if (isMajor && this.getOption('showMajorLabels')) {
  3926. if (x > 0) {
  3927. if (xFirstMajorLabel == undefined) {
  3928. xFirstMajorLabel = x;
  3929. }
  3930. this._repaintMajorText(x, step.getLabelMajor());
  3931. }
  3932. this._repaintMajorLine(x);
  3933. }
  3934. else {
  3935. this._repaintMinorLine(x);
  3936. }
  3937. step.next();
  3938. }
  3939. // create a major label on the left when needed
  3940. if (this.getOption('showMajorLabels')) {
  3941. var leftTime = this.toTime(0),
  3942. leftText = step.getLabelMajor(leftTime),
  3943. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  3944. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  3945. this._repaintMajorText(0, leftText);
  3946. }
  3947. }
  3948. this._repaintEnd();
  3949. }
  3950. this._repaintLine();
  3951. // put frame online again
  3952. if (beforeChild) {
  3953. parent.insertBefore(frame, beforeChild);
  3954. }
  3955. else {
  3956. parent.appendChild(frame)
  3957. }
  3958. }
  3959. return (changed > 0);
  3960. };
  3961. /**
  3962. * Start a repaint. Move all DOM elements to a redundant list, where they
  3963. * can be picked for re-use, or can be cleaned up in the end
  3964. * @private
  3965. */
  3966. TimeAxis.prototype._repaintStart = function () {
  3967. var dom = this.dom,
  3968. redundant = dom.redundant;
  3969. redundant.majorLines = dom.majorLines;
  3970. redundant.majorTexts = dom.majorTexts;
  3971. redundant.minorLines = dom.minorLines;
  3972. redundant.minorTexts = dom.minorTexts;
  3973. dom.majorLines = [];
  3974. dom.majorTexts = [];
  3975. dom.minorLines = [];
  3976. dom.minorTexts = [];
  3977. };
  3978. /**
  3979. * End a repaint. Cleanup leftover DOM elements in the redundant list
  3980. * @private
  3981. */
  3982. TimeAxis.prototype._repaintEnd = function () {
  3983. util.forEach(this.dom.redundant, function (arr) {
  3984. while (arr.length) {
  3985. var elem = arr.pop();
  3986. if (elem && elem.parentNode) {
  3987. elem.parentNode.removeChild(elem);
  3988. }
  3989. }
  3990. });
  3991. };
  3992. /**
  3993. * Create a minor label for the axis at position x
  3994. * @param {Number} x
  3995. * @param {String} text
  3996. * @private
  3997. */
  3998. TimeAxis.prototype._repaintMinorText = function (x, text) {
  3999. // reuse redundant label
  4000. var label = this.dom.redundant.minorTexts.shift();
  4001. if (!label) {
  4002. // create new label
  4003. var content = document.createTextNode('');
  4004. label = document.createElement('div');
  4005. label.appendChild(content);
  4006. label.className = 'text minor';
  4007. this.frame.appendChild(label);
  4008. }
  4009. this.dom.minorTexts.push(label);
  4010. label.childNodes[0].nodeValue = text;
  4011. label.style.left = x + 'px';
  4012. label.style.top = this.props.minorLabelTop + 'px';
  4013. //label.title = title; // TODO: this is a heavy operation
  4014. };
  4015. /**
  4016. * Create a Major label for the axis at position x
  4017. * @param {Number} x
  4018. * @param {String} text
  4019. * @private
  4020. */
  4021. TimeAxis.prototype._repaintMajorText = function (x, text) {
  4022. // reuse redundant label
  4023. var label = this.dom.redundant.majorTexts.shift();
  4024. if (!label) {
  4025. // create label
  4026. var content = document.createTextNode(text);
  4027. label = document.createElement('div');
  4028. label.className = 'text major';
  4029. label.appendChild(content);
  4030. this.frame.appendChild(label);
  4031. }
  4032. this.dom.majorTexts.push(label);
  4033. label.childNodes[0].nodeValue = text;
  4034. label.style.top = this.props.majorLabelTop + 'px';
  4035. label.style.left = x + 'px';
  4036. //label.title = title; // TODO: this is a heavy operation
  4037. };
  4038. /**
  4039. * Create a minor line for the axis at position x
  4040. * @param {Number} x
  4041. * @private
  4042. */
  4043. TimeAxis.prototype._repaintMinorLine = function (x) {
  4044. // reuse redundant line
  4045. var line = this.dom.redundant.minorLines.shift();
  4046. if (!line) {
  4047. // create vertical line
  4048. line = document.createElement('div');
  4049. line.className = 'grid vertical minor';
  4050. this.frame.appendChild(line);
  4051. }
  4052. this.dom.minorLines.push(line);
  4053. var props = this.props;
  4054. line.style.top = props.minorLineTop + 'px';
  4055. line.style.height = props.minorLineHeight + 'px';
  4056. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  4057. };
  4058. /**
  4059. * Create a Major line for the axis at position x
  4060. * @param {Number} x
  4061. * @private
  4062. */
  4063. TimeAxis.prototype._repaintMajorLine = function (x) {
  4064. // reuse redundant line
  4065. var line = this.dom.redundant.majorLines.shift();
  4066. if (!line) {
  4067. // create vertical line
  4068. line = document.createElement('DIV');
  4069. line.className = 'grid vertical major';
  4070. this.frame.appendChild(line);
  4071. }
  4072. this.dom.majorLines.push(line);
  4073. var props = this.props;
  4074. line.style.top = props.majorLineTop + 'px';
  4075. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  4076. line.style.height = props.majorLineHeight + 'px';
  4077. };
  4078. /**
  4079. * Repaint the horizontal line for the axis
  4080. * @private
  4081. */
  4082. TimeAxis.prototype._repaintLine = function() {
  4083. var line = this.dom.line,
  4084. frame = this.frame,
  4085. options = this.options;
  4086. // line before all axis elements
  4087. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  4088. if (line) {
  4089. // put this line at the end of all childs
  4090. frame.removeChild(line);
  4091. frame.appendChild(line);
  4092. }
  4093. else {
  4094. // create the axis line
  4095. line = document.createElement('div');
  4096. line.className = 'grid horizontal major';
  4097. frame.appendChild(line);
  4098. this.dom.line = line;
  4099. }
  4100. line.style.top = this.props.lineTop + 'px';
  4101. }
  4102. else {
  4103. if (line && line.parentElement) {
  4104. frame.removeChild(line.line);
  4105. delete this.dom.line;
  4106. }
  4107. }
  4108. };
  4109. /**
  4110. * Create characters used to determine the size of text on the axis
  4111. * @private
  4112. */
  4113. TimeAxis.prototype._repaintMeasureChars = function () {
  4114. // calculate the width and height of a single character
  4115. // this is used to calculate the step size, and also the positioning of the
  4116. // axis
  4117. var dom = this.dom,
  4118. text;
  4119. if (!dom.measureCharMinor) {
  4120. text = document.createTextNode('0');
  4121. var measureCharMinor = document.createElement('DIV');
  4122. measureCharMinor.className = 'text minor measure';
  4123. measureCharMinor.appendChild(text);
  4124. this.frame.appendChild(measureCharMinor);
  4125. dom.measureCharMinor = measureCharMinor;
  4126. }
  4127. if (!dom.measureCharMajor) {
  4128. text = document.createTextNode('0');
  4129. var measureCharMajor = document.createElement('DIV');
  4130. measureCharMajor.className = 'text major measure';
  4131. measureCharMajor.appendChild(text);
  4132. this.frame.appendChild(measureCharMajor);
  4133. dom.measureCharMajor = measureCharMajor;
  4134. }
  4135. };
  4136. /**
  4137. * Reflow the component
  4138. * @return {Boolean} resized
  4139. */
  4140. TimeAxis.prototype.reflow = function () {
  4141. var changed = 0,
  4142. update = util.updateProperty,
  4143. frame = this.frame,
  4144. range = this.range;
  4145. if (!range) {
  4146. throw new Error('Cannot repaint time axis: no range configured');
  4147. }
  4148. if (frame) {
  4149. changed += update(this, 'top', frame.offsetTop);
  4150. changed += update(this, 'left', frame.offsetLeft);
  4151. // calculate size of a character
  4152. var props = this.props,
  4153. showMinorLabels = this.getOption('showMinorLabels'),
  4154. showMajorLabels = this.getOption('showMajorLabels'),
  4155. measureCharMinor = this.dom.measureCharMinor,
  4156. measureCharMajor = this.dom.measureCharMajor;
  4157. if (measureCharMinor) {
  4158. props.minorCharHeight = measureCharMinor.clientHeight;
  4159. props.minorCharWidth = measureCharMinor.clientWidth;
  4160. }
  4161. if (measureCharMajor) {
  4162. props.majorCharHeight = measureCharMajor.clientHeight;
  4163. props.majorCharWidth = measureCharMajor.clientWidth;
  4164. }
  4165. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  4166. if (parentHeight != props.parentHeight) {
  4167. props.parentHeight = parentHeight;
  4168. changed += 1;
  4169. }
  4170. switch (this.getOption('orientation')) {
  4171. case 'bottom':
  4172. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4173. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4174. props.minorLabelTop = 0;
  4175. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  4176. props.minorLineTop = -this.top;
  4177. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  4178. props.minorLineWidth = 1; // TODO: really calculate width
  4179. props.majorLineTop = -this.top;
  4180. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  4181. props.majorLineWidth = 1; // TODO: really calculate width
  4182. props.lineTop = 0;
  4183. break;
  4184. case 'top':
  4185. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  4186. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  4187. props.majorLabelTop = 0;
  4188. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  4189. props.minorLineTop = props.minorLabelTop;
  4190. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  4191. props.minorLineWidth = 1; // TODO: really calculate width
  4192. props.majorLineTop = 0;
  4193. props.majorLineHeight = Math.max(parentHeight - this.top);
  4194. props.majorLineWidth = 1; // TODO: really calculate width
  4195. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  4196. break;
  4197. default:
  4198. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  4199. }
  4200. var height = props.minorLabelHeight + props.majorLabelHeight;
  4201. changed += update(this, 'width', frame.offsetWidth);
  4202. changed += update(this, 'height', height);
  4203. // calculate range and step
  4204. this._updateConversion();
  4205. var start = util.convert(range.start, 'Number'),
  4206. end = util.convert(range.end, 'Number'),
  4207. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  4208. -this.toTime(0).valueOf();
  4209. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  4210. changed += update(props.range, 'start', start);
  4211. changed += update(props.range, 'end', end);
  4212. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  4213. }
  4214. return (changed > 0);
  4215. };
  4216. /**
  4217. * Calculate the scale and offset to convert a position on screen to the
  4218. * corresponding date and vice versa.
  4219. * After the method _updateConversion is executed once, the methods toTime
  4220. * and toScreen can be used.
  4221. * @private
  4222. */
  4223. TimeAxis.prototype._updateConversion = function() {
  4224. var range = this.range;
  4225. if (!range) {
  4226. throw new Error('No range configured');
  4227. }
  4228. if (range.conversion) {
  4229. this.conversion = range.conversion(this.width);
  4230. }
  4231. else {
  4232. this.conversion = Range.conversion(range.start, range.end, this.width);
  4233. }
  4234. };
  4235. /**
  4236. * A current time bar
  4237. * @param {Component} parent
  4238. * @param {Component[]} [depends] Components on which this components depends
  4239. * (except for the parent)
  4240. * @param {Object} [options] Available parameters:
  4241. * {Boolean} [showCurrentTime]
  4242. * @constructor CurrentTime
  4243. * @extends Component
  4244. */
  4245. function CurrentTime (parent, depends, options) {
  4246. this.id = util.randomUUID();
  4247. this.parent = parent;
  4248. this.depends = depends;
  4249. this.options = options || {};
  4250. this.defaultOptions = {
  4251. showCurrentTime: false
  4252. };
  4253. }
  4254. CurrentTime.prototype = new Component();
  4255. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  4256. /**
  4257. * Get the container element of the bar, which can be used by a child to
  4258. * add its own widgets.
  4259. * @returns {HTMLElement} container
  4260. */
  4261. CurrentTime.prototype.getContainer = function () {
  4262. return this.frame;
  4263. };
  4264. /**
  4265. * Repaint the component
  4266. * @return {Boolean} changed
  4267. */
  4268. CurrentTime.prototype.repaint = function () {
  4269. var bar = this.frame,
  4270. parent = this.parent,
  4271. parentContainer = parent.parent.getContainer();
  4272. if (!parent) {
  4273. throw new Error('Cannot repaint bar: no parent attached');
  4274. }
  4275. if (!parentContainer) {
  4276. throw new Error('Cannot repaint bar: parent has no container element');
  4277. }
  4278. if (!this.getOption('showCurrentTime')) {
  4279. if (bar) {
  4280. parentContainer.removeChild(bar);
  4281. delete this.frame;
  4282. }
  4283. return;
  4284. }
  4285. if (!bar) {
  4286. bar = document.createElement('div');
  4287. bar.className = 'currenttime';
  4288. bar.style.position = 'absolute';
  4289. bar.style.top = '0px';
  4290. bar.style.height = '100%';
  4291. parentContainer.appendChild(bar);
  4292. this.frame = bar;
  4293. }
  4294. if (!parent.conversion) {
  4295. parent._updateConversion();
  4296. }
  4297. var now = new Date();
  4298. var x = parent.toScreen(now);
  4299. bar.style.left = x + 'px';
  4300. bar.title = 'Current time: ' + now;
  4301. // start a timer to adjust for the new time
  4302. if (this.currentTimeTimer !== undefined) {
  4303. clearTimeout(this.currentTimeTimer);
  4304. delete this.currentTimeTimer;
  4305. }
  4306. var timeline = this;
  4307. var interval = 1 / parent.conversion.scale / 2;
  4308. if (interval < 30) {
  4309. interval = 30;
  4310. }
  4311. this.currentTimeTimer = setTimeout(function() {
  4312. timeline.repaint();
  4313. }, interval);
  4314. return false;
  4315. };
  4316. /**
  4317. * A custom time bar
  4318. * @param {Component} parent
  4319. * @param {Component[]} [depends] Components on which this components depends
  4320. * (except for the parent)
  4321. * @param {Object} [options] Available parameters:
  4322. * {Boolean} [showCustomTime]
  4323. * @constructor CustomTime
  4324. * @extends Component
  4325. */
  4326. function CustomTime (parent, depends, options) {
  4327. this.id = util.randomUUID();
  4328. this.parent = parent;
  4329. this.depends = depends;
  4330. this.options = options || {};
  4331. this.defaultOptions = {
  4332. showCustomTime: false
  4333. };
  4334. this.listeners = [];
  4335. this.customTime = new Date();
  4336. }
  4337. CustomTime.prototype = new Component();
  4338. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  4339. /**
  4340. * Get the container element of the bar, which can be used by a child to
  4341. * add its own widgets.
  4342. * @returns {HTMLElement} container
  4343. */
  4344. CustomTime.prototype.getContainer = function () {
  4345. return this.frame;
  4346. };
  4347. /**
  4348. * Repaint the component
  4349. * @return {Boolean} changed
  4350. */
  4351. CustomTime.prototype.repaint = function () {
  4352. var bar = this.frame,
  4353. parent = this.parent,
  4354. parentContainer = parent.parent.getContainer();
  4355. if (!parent) {
  4356. throw new Error('Cannot repaint bar: no parent attached');
  4357. }
  4358. if (!parentContainer) {
  4359. throw new Error('Cannot repaint bar: parent has no container element');
  4360. }
  4361. if (!this.getOption('showCustomTime')) {
  4362. if (bar) {
  4363. parentContainer.removeChild(bar);
  4364. delete this.frame;
  4365. }
  4366. return;
  4367. }
  4368. if (!bar) {
  4369. bar = document.createElement('div');
  4370. bar.className = 'customtime';
  4371. bar.style.position = 'absolute';
  4372. bar.style.top = '0px';
  4373. bar.style.height = '100%';
  4374. parentContainer.appendChild(bar);
  4375. var drag = document.createElement('div');
  4376. drag.style.position = 'relative';
  4377. drag.style.top = '0px';
  4378. drag.style.left = '-10px';
  4379. drag.style.height = '100%';
  4380. drag.style.width = '20px';
  4381. bar.appendChild(drag);
  4382. this.frame = bar;
  4383. this.subscribe(this, 'movetime');
  4384. }
  4385. if (!parent.conversion) {
  4386. parent._updateConversion();
  4387. }
  4388. var x = parent.toScreen(this.customTime);
  4389. bar.style.left = x + 'px';
  4390. bar.title = 'Time: ' + this.customTime;
  4391. return false;
  4392. };
  4393. /**
  4394. * Set custom time.
  4395. * @param {Date} time
  4396. */
  4397. CustomTime.prototype._setCustomTime = function(time) {
  4398. this.customTime = new Date(time.valueOf());
  4399. this.repaint();
  4400. };
  4401. /**
  4402. * Retrieve the current custom time.
  4403. * @return {Date} customTime
  4404. */
  4405. CustomTime.prototype._getCustomTime = function() {
  4406. return new Date(this.customTime.valueOf());
  4407. };
  4408. /**
  4409. * Add listeners for mouse and touch events to the component
  4410. * @param {Component} component
  4411. */
  4412. CustomTime.prototype.subscribe = function (component, event) {
  4413. var me = this;
  4414. var listener = {
  4415. component: component,
  4416. event: event,
  4417. callback: function (event) {
  4418. me._onMouseDown(event, listener);
  4419. },
  4420. params: {}
  4421. };
  4422. component.on('mousedown', listener.callback);
  4423. me.listeners.push(listener);
  4424. };
  4425. /**
  4426. * Event handler
  4427. * @param {String} event name of the event, for example 'click', 'mousemove'
  4428. * @param {function} callback callback handler, invoked with the raw HTML Event
  4429. * as parameter.
  4430. */
  4431. CustomTime.prototype.on = function (event, callback) {
  4432. var bar = this.frame;
  4433. if (!bar) {
  4434. throw new Error('Cannot add event listener: no parent attached');
  4435. }
  4436. events.addListener(this, event, callback);
  4437. util.addEventListener(bar, event, callback);
  4438. };
  4439. /**
  4440. * Start moving horizontally
  4441. * @param {Event} event
  4442. * @param {Object} listener Listener containing the component and params
  4443. * @private
  4444. */
  4445. CustomTime.prototype._onMouseDown = function(event, listener) {
  4446. event = event || window.event;
  4447. var params = listener.params;
  4448. // only react on left mouse button down
  4449. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  4450. if (!leftButtonDown) {
  4451. return;
  4452. }
  4453. // get mouse position
  4454. params.mouseX = util.getPageX(event);
  4455. params.moved = false;
  4456. params.customTime = this.customTime;
  4457. // add event listeners to handle moving the custom time bar
  4458. var me = this;
  4459. if (!params.onMouseMove) {
  4460. params.onMouseMove = function (event) {
  4461. me._onMouseMove(event, listener);
  4462. };
  4463. util.addEventListener(document, 'mousemove', params.onMouseMove);
  4464. }
  4465. if (!params.onMouseUp) {
  4466. params.onMouseUp = function (event) {
  4467. me._onMouseUp(event, listener);
  4468. };
  4469. util.addEventListener(document, 'mouseup', params.onMouseUp);
  4470. }
  4471. util.stopPropagation(event);
  4472. util.preventDefault(event);
  4473. };
  4474. /**
  4475. * Perform moving operating.
  4476. * This function activated from within the funcion CustomTime._onMouseDown().
  4477. * @param {Event} event
  4478. * @param {Object} listener
  4479. * @private
  4480. */
  4481. CustomTime.prototype._onMouseMove = function (event, listener) {
  4482. event = event || window.event;
  4483. var params = listener.params;
  4484. var parent = this.parent;
  4485. // calculate change in mouse position
  4486. var mouseX = util.getPageX(event);
  4487. if (params.mouseX === undefined) {
  4488. params.mouseX = mouseX;
  4489. }
  4490. var diff = mouseX - params.mouseX;
  4491. // if mouse movement is big enough, register it as a "moved" event
  4492. if (Math.abs(diff) >= 1) {
  4493. params.moved = true;
  4494. }
  4495. var x = parent.toScreen(params.customTime);
  4496. var xnew = x + diff;
  4497. var time = parent.toTime(xnew);
  4498. this._setCustomTime(time);
  4499. // fire a timechange event
  4500. events.trigger(this, 'timechange', {customTime: this.customTime});
  4501. util.preventDefault(event);
  4502. };
  4503. /**
  4504. * Stop moving operating.
  4505. * This function activated from within the function CustomTime._onMouseDown().
  4506. * @param {event} event
  4507. * @param {Object} listener
  4508. * @private
  4509. */
  4510. CustomTime.prototype._onMouseUp = function (event, listener) {
  4511. event = event || window.event;
  4512. var params = listener.params;
  4513. // remove event listeners here, important for Safari
  4514. if (params.onMouseMove) {
  4515. util.removeEventListener(document, 'mousemove', params.onMouseMove);
  4516. params.onMouseMove = null;
  4517. }
  4518. if (params.onMouseUp) {
  4519. util.removeEventListener(document, 'mouseup', params.onMouseUp);
  4520. params.onMouseUp = null;
  4521. }
  4522. if (params.moved) {
  4523. // fire a timechanged event
  4524. events.trigger(this, 'timechanged', {customTime: this.customTime});
  4525. }
  4526. };
  4527. /**
  4528. * An ItemSet holds a set of items and ranges which can be displayed in a
  4529. * range. The width is determined by the parent of the ItemSet, and the height
  4530. * is determined by the size of the items.
  4531. * @param {Component} parent
  4532. * @param {Component[]} [depends] Components on which this components depends
  4533. * (except for the parent)
  4534. * @param {Object} [options] See ItemSet.setOptions for the available
  4535. * options.
  4536. * @constructor ItemSet
  4537. * @extends Panel
  4538. */
  4539. // TODO: improve performance by replacing all Array.forEach with a for loop
  4540. function ItemSet(parent, depends, options) {
  4541. this.id = util.randomUUID();
  4542. this.parent = parent;
  4543. this.depends = depends;
  4544. // one options object is shared by this itemset and all its items
  4545. this.options = options || {};
  4546. this.defaultOptions = {
  4547. type: 'box',
  4548. align: 'center',
  4549. orientation: 'bottom',
  4550. margin: {
  4551. axis: 20,
  4552. item: 10
  4553. },
  4554. padding: 5
  4555. };
  4556. this.dom = {};
  4557. var me = this;
  4558. this.itemsData = null; // DataSet
  4559. this.range = null; // Range or Object {start: number, end: number}
  4560. this.listeners = {
  4561. 'add': function (event, params, senderId) {
  4562. if (senderId != me.id) {
  4563. me._onAdd(params.items);
  4564. }
  4565. },
  4566. 'update': function (event, params, senderId) {
  4567. if (senderId != me.id) {
  4568. me._onUpdate(params.items);
  4569. }
  4570. },
  4571. 'remove': function (event, params, senderId) {
  4572. if (senderId != me.id) {
  4573. me._onRemove(params.items);
  4574. }
  4575. }
  4576. };
  4577. this.items = {}; // object with an Item for every data item
  4578. this.selection = []; // list with the ids of all selected nodes
  4579. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  4580. this.stack = new Stack(this, Object.create(this.options));
  4581. this.conversion = null;
  4582. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  4583. }
  4584. ItemSet.prototype = new Panel();
  4585. // available item types will be registered here
  4586. ItemSet.types = {
  4587. box: ItemBox,
  4588. range: ItemRange,
  4589. rangeoverflow: ItemRangeOverflow,
  4590. point: ItemPoint
  4591. };
  4592. /**
  4593. * Set options for the ItemSet. Existing options will be extended/overwritten.
  4594. * @param {Object} [options] The following options are available:
  4595. * {String | function} [className]
  4596. * class name for the itemset
  4597. * {String} [type]
  4598. * Default type for the items. Choose from 'box'
  4599. * (default), 'point', or 'range'. The default
  4600. * Style can be overwritten by individual items.
  4601. * {String} align
  4602. * Alignment for the items, only applicable for
  4603. * ItemBox. Choose 'center' (default), 'left', or
  4604. * 'right'.
  4605. * {String} orientation
  4606. * Orientation of the item set. Choose 'top' or
  4607. * 'bottom' (default).
  4608. * {Number} margin.axis
  4609. * Margin between the axis and the items in pixels.
  4610. * Default is 20.
  4611. * {Number} margin.item
  4612. * Margin between items in pixels. Default is 10.
  4613. * {Number} padding
  4614. * Padding of the contents of an item in pixels.
  4615. * Must correspond with the items css. Default is 5.
  4616. */
  4617. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  4618. /**
  4619. * Set range (start and end).
  4620. * @param {Range | Object} range A Range or an object containing start and end.
  4621. */
  4622. ItemSet.prototype.setRange = function setRange(range) {
  4623. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  4624. throw new TypeError('Range must be an instance of Range, ' +
  4625. 'or an object containing start and end.');
  4626. }
  4627. this.range = range;
  4628. };
  4629. /**
  4630. * Change the item selection, and/or get currently selected items
  4631. * @param {Array} [ids] An array with zero or more ids of the items to be selected.
  4632. * @return {Array} ids The ids of the selected items
  4633. */
  4634. ItemSet.prototype.select = function select(ids) {
  4635. var i, ii, id, item, selection;
  4636. if (ids) {
  4637. if (!Array.isArray(ids)) {
  4638. throw new TypeError('Array expected');
  4639. }
  4640. // unselect currently selected items
  4641. for (i = 0, ii = this.selection.length; i < ii; i++) {
  4642. id = this.selection[i];
  4643. item = this.items[id];
  4644. if (item) item.unselect();
  4645. }
  4646. // select items
  4647. this.selection = [];
  4648. for (i = 0, ii = ids.length; i < ii; i++) {
  4649. id = ids[i];
  4650. item = this.items[id];
  4651. if (item) {
  4652. this.selection.push(id);
  4653. item.select();
  4654. }
  4655. }
  4656. // trigger a select event
  4657. selection = this.selection.concat([]);
  4658. events.trigger(this, 'select', {
  4659. ids: selection
  4660. });
  4661. if (this.controller) {
  4662. this.requestRepaint();
  4663. }
  4664. }
  4665. else {
  4666. selection = this.selection.concat([]);
  4667. }
  4668. return selection;
  4669. };
  4670. /**
  4671. * Deselect a selected item
  4672. * @param {String | Number} id
  4673. * @private
  4674. */
  4675. ItemSet.prototype._deselect = function _deselect(id) {
  4676. var selection = this.selection;
  4677. for (var i = 0, ii = selection.length; i < ii; i++) {
  4678. if (selection[i] == id) { // non-strict comparison!
  4679. selection.splice(i, 1);
  4680. break;
  4681. }
  4682. }
  4683. };
  4684. /**
  4685. * Repaint the component
  4686. * @return {Boolean} changed
  4687. */
  4688. ItemSet.prototype.repaint = function repaint() {
  4689. var changed = 0,
  4690. update = util.updateProperty,
  4691. asSize = util.option.asSize,
  4692. options = this.options,
  4693. orientation = this.getOption('orientation'),
  4694. defaultOptions = this.defaultOptions,
  4695. frame = this.frame;
  4696. if (!frame) {
  4697. frame = document.createElement('div');
  4698. frame.className = 'itemset';
  4699. var className = options.className;
  4700. if (className) {
  4701. util.addClassName(frame, util.option.asString(className));
  4702. }
  4703. // create background panel
  4704. var background = document.createElement('div');
  4705. background.className = 'background';
  4706. frame.appendChild(background);
  4707. this.dom.background = background;
  4708. // create foreground panel
  4709. var foreground = document.createElement('div');
  4710. foreground.className = 'foreground';
  4711. frame.appendChild(foreground);
  4712. this.dom.foreground = foreground;
  4713. // create axis panel
  4714. var axis = document.createElement('div');
  4715. axis.className = 'itemset-axis';
  4716. //frame.appendChild(axis);
  4717. this.dom.axis = axis;
  4718. this.frame = frame;
  4719. changed += 1;
  4720. }
  4721. if (!this.parent) {
  4722. throw new Error('Cannot repaint itemset: no parent attached');
  4723. }
  4724. var parentContainer = this.parent.getContainer();
  4725. if (!parentContainer) {
  4726. throw new Error('Cannot repaint itemset: parent has no container element');
  4727. }
  4728. if (!frame.parentNode) {
  4729. parentContainer.appendChild(frame);
  4730. changed += 1;
  4731. }
  4732. if (!this.dom.axis.parentNode) {
  4733. parentContainer.appendChild(this.dom.axis);
  4734. changed += 1;
  4735. }
  4736. // reposition frame
  4737. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  4738. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  4739. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  4740. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  4741. // reposition axis
  4742. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  4743. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  4744. if (orientation == 'bottom') {
  4745. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  4746. }
  4747. else { // orientation == 'top'
  4748. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  4749. }
  4750. this._updateConversion();
  4751. var me = this,
  4752. queue = this.queue,
  4753. itemsData = this.itemsData,
  4754. items = this.items,
  4755. dataOptions = {
  4756. // TODO: cleanup
  4757. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  4758. };
  4759. // show/hide added/changed/removed items
  4760. Object.keys(queue).forEach(function (id) {
  4761. //var entry = queue[id];
  4762. var action = queue[id];
  4763. var item = items[id];
  4764. //var item = entry.item;
  4765. //noinspection FallthroughInSwitchStatementJS
  4766. switch (action) {
  4767. case 'add':
  4768. case 'update':
  4769. var itemData = itemsData && itemsData.get(id, dataOptions);
  4770. if (itemData) {
  4771. var type = itemData.type ||
  4772. (itemData.start && itemData.end && 'range') ||
  4773. options.type ||
  4774. 'box';
  4775. var constructor = ItemSet.types[type];
  4776. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  4777. if (item) {
  4778. // update item
  4779. if (!constructor || !(item instanceof constructor)) {
  4780. // item type has changed, hide and delete the item
  4781. changed += item.hide();
  4782. item = null;
  4783. }
  4784. else {
  4785. item.data = itemData; // TODO: create a method item.setData ?
  4786. changed++;
  4787. }
  4788. }
  4789. if (!item) {
  4790. // create item
  4791. if (constructor) {
  4792. item = new constructor(me, itemData, options, defaultOptions);
  4793. item.id = id;
  4794. changed++;
  4795. }
  4796. else {
  4797. throw new TypeError('Unknown item type "' + type + '"');
  4798. }
  4799. }
  4800. // force a repaint (not only a reposition)
  4801. item.repaint();
  4802. items[id] = item;
  4803. }
  4804. // update queue
  4805. delete queue[id];
  4806. break;
  4807. case 'remove':
  4808. if (item) {
  4809. // remove the item from the set selected items
  4810. if (item.selected) {
  4811. me._deselect(id);
  4812. }
  4813. // remove DOM of the item
  4814. changed += item.hide();
  4815. }
  4816. // update lists
  4817. delete items[id];
  4818. delete queue[id];
  4819. break;
  4820. default:
  4821. console.log('Error: unknown action "' + action + '"');
  4822. }
  4823. });
  4824. // reposition all items. Show items only when in the visible area
  4825. util.forEach(this.items, function (item) {
  4826. if (item.visible) {
  4827. changed += item.show();
  4828. item.reposition();
  4829. }
  4830. else {
  4831. changed += item.hide();
  4832. }
  4833. });
  4834. return (changed > 0);
  4835. };
  4836. /**
  4837. * Get the foreground container element
  4838. * @return {HTMLElement} foreground
  4839. */
  4840. ItemSet.prototype.getForeground = function getForeground() {
  4841. return this.dom.foreground;
  4842. };
  4843. /**
  4844. * Get the background container element
  4845. * @return {HTMLElement} background
  4846. */
  4847. ItemSet.prototype.getBackground = function getBackground() {
  4848. return this.dom.background;
  4849. };
  4850. /**
  4851. * Get the axis container element
  4852. * @return {HTMLElement} axis
  4853. */
  4854. ItemSet.prototype.getAxis = function getAxis() {
  4855. return this.dom.axis;
  4856. };
  4857. /**
  4858. * Reflow the component
  4859. * @return {Boolean} resized
  4860. */
  4861. ItemSet.prototype.reflow = function reflow () {
  4862. var changed = 0,
  4863. options = this.options,
  4864. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  4865. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  4866. update = util.updateProperty,
  4867. asNumber = util.option.asNumber,
  4868. asSize = util.option.asSize,
  4869. frame = this.frame;
  4870. if (frame) {
  4871. this._updateConversion();
  4872. util.forEach(this.items, function (item) {
  4873. changed += item.reflow();
  4874. });
  4875. // TODO: stack.update should be triggered via an event, in stack itself
  4876. // TODO: only update the stack when there are changed items
  4877. this.stack.update();
  4878. var maxHeight = asNumber(options.maxHeight);
  4879. var fixedHeight = (asSize(options.height) != null);
  4880. var height;
  4881. if (fixedHeight) {
  4882. height = frame.offsetHeight;
  4883. }
  4884. else {
  4885. // height is not specified, determine the height from the height and positioned items
  4886. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  4887. if (visibleItems.length) {
  4888. var min = visibleItems[0].top;
  4889. var max = visibleItems[0].top + visibleItems[0].height;
  4890. util.forEach(visibleItems, function (item) {
  4891. min = Math.min(min, item.top);
  4892. max = Math.max(max, (item.top + item.height));
  4893. });
  4894. height = (max - min) + marginAxis + marginItem;
  4895. }
  4896. else {
  4897. height = marginAxis + marginItem;
  4898. }
  4899. }
  4900. if (maxHeight != null) {
  4901. height = Math.min(height, maxHeight);
  4902. }
  4903. changed += update(this, 'height', height);
  4904. // calculate height from items
  4905. changed += update(this, 'top', frame.offsetTop);
  4906. changed += update(this, 'left', frame.offsetLeft);
  4907. changed += update(this, 'width', frame.offsetWidth);
  4908. }
  4909. else {
  4910. changed += 1;
  4911. }
  4912. return (changed > 0);
  4913. };
  4914. /**
  4915. * Hide this component from the DOM
  4916. * @return {Boolean} changed
  4917. */
  4918. ItemSet.prototype.hide = function hide() {
  4919. var changed = false;
  4920. // remove the DOM
  4921. if (this.frame && this.frame.parentNode) {
  4922. this.frame.parentNode.removeChild(this.frame);
  4923. changed = true;
  4924. }
  4925. if (this.dom.axis && this.dom.axis.parentNode) {
  4926. this.dom.axis.parentNode.removeChild(this.dom.axis);
  4927. changed = true;
  4928. }
  4929. return changed;
  4930. };
  4931. /**
  4932. * Set items
  4933. * @param {vis.DataSet | null} items
  4934. */
  4935. ItemSet.prototype.setItems = function setItems(items) {
  4936. var me = this,
  4937. ids,
  4938. oldItemsData = this.itemsData;
  4939. // replace the dataset
  4940. if (!items) {
  4941. this.itemsData = null;
  4942. }
  4943. else if (items instanceof DataSet || items instanceof DataView) {
  4944. this.itemsData = items;
  4945. }
  4946. else {
  4947. throw new TypeError('Data must be an instance of DataSet');
  4948. }
  4949. if (oldItemsData) {
  4950. // unsubscribe from old dataset
  4951. util.forEach(this.listeners, function (callback, event) {
  4952. oldItemsData.unsubscribe(event, callback);
  4953. });
  4954. // remove all drawn items
  4955. ids = oldItemsData.getIds();
  4956. this._onRemove(ids);
  4957. }
  4958. if (this.itemsData) {
  4959. // subscribe to new dataset
  4960. var id = this.id;
  4961. util.forEach(this.listeners, function (callback, event) {
  4962. me.itemsData.subscribe(event, callback, id);
  4963. });
  4964. // draw all new items
  4965. ids = this.itemsData.getIds();
  4966. this._onAdd(ids);
  4967. }
  4968. };
  4969. /**
  4970. * Get the current items items
  4971. * @returns {vis.DataSet | null}
  4972. */
  4973. ItemSet.prototype.getItems = function getItems() {
  4974. return this.itemsData;
  4975. };
  4976. /**
  4977. * Handle updated items
  4978. * @param {Number[]} ids
  4979. * @private
  4980. */
  4981. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  4982. this._toQueue('update', ids);
  4983. };
  4984. /**
  4985. * Handle changed items
  4986. * @param {Number[]} ids
  4987. * @private
  4988. */
  4989. ItemSet.prototype._onAdd = function _onAdd(ids) {
  4990. this._toQueue('add', ids);
  4991. };
  4992. /**
  4993. * Handle removed items
  4994. * @param {Number[]} ids
  4995. * @private
  4996. */
  4997. ItemSet.prototype._onRemove = function _onRemove(ids) {
  4998. this._toQueue('remove', ids);
  4999. };
  5000. /**
  5001. * Put items in the queue to be added/updated/remove
  5002. * @param {String} action can be 'add', 'update', 'remove'
  5003. * @param {Number[]} ids
  5004. */
  5005. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  5006. var queue = this.queue;
  5007. ids.forEach(function (id) {
  5008. queue[id] = action;
  5009. });
  5010. if (this.controller) {
  5011. //this.requestReflow();
  5012. this.requestRepaint();
  5013. }
  5014. };
  5015. /**
  5016. * Calculate the scale and offset to convert a position on screen to the
  5017. * corresponding date and vice versa.
  5018. * After the method _updateConversion is executed once, the methods toTime
  5019. * and toScreen can be used.
  5020. * @private
  5021. */
  5022. ItemSet.prototype._updateConversion = function _updateConversion() {
  5023. var range = this.range;
  5024. if (!range) {
  5025. throw new Error('No range configured');
  5026. }
  5027. if (range.conversion) {
  5028. this.conversion = range.conversion(this.width);
  5029. }
  5030. else {
  5031. this.conversion = Range.conversion(range.start, range.end, this.width);
  5032. }
  5033. };
  5034. /**
  5035. * Convert a position on screen (pixels) to a datetime
  5036. * Before this method can be used, the method _updateConversion must be
  5037. * executed once.
  5038. * @param {int} x Position on the screen in pixels
  5039. * @return {Date} time The datetime the corresponds with given position x
  5040. */
  5041. ItemSet.prototype.toTime = function toTime(x) {
  5042. var conversion = this.conversion;
  5043. return new Date(x / conversion.scale + conversion.offset);
  5044. };
  5045. /**
  5046. * Convert a datetime (Date object) into a position on the screen
  5047. * Before this method can be used, the method _updateConversion must be
  5048. * executed once.
  5049. * @param {Date} time A date
  5050. * @return {int} x The position on the screen in pixels which corresponds
  5051. * with the given date.
  5052. */
  5053. ItemSet.prototype.toScreen = function toScreen(time) {
  5054. var conversion = this.conversion;
  5055. return (time.valueOf() - conversion.offset) * conversion.scale;
  5056. };
  5057. /**
  5058. * @constructor Item
  5059. * @param {ItemSet} parent
  5060. * @param {Object} data Object containing (optional) parameters type,
  5061. * start, end, content, group, className.
  5062. * @param {Object} [options] Options to set initial property values
  5063. * @param {Object} [defaultOptions] default options
  5064. * // TODO: describe available options
  5065. */
  5066. function Item (parent, data, options, defaultOptions) {
  5067. this.parent = parent;
  5068. this.data = data;
  5069. this.dom = null;
  5070. this.options = options || {};
  5071. this.defaultOptions = defaultOptions || {};
  5072. this.selected = false;
  5073. this.visible = false;
  5074. this.top = 0;
  5075. this.left = 0;
  5076. this.width = 0;
  5077. this.height = 0;
  5078. }
  5079. /**
  5080. * Select current item
  5081. */
  5082. Item.prototype.select = function select() {
  5083. this.selected = true;
  5084. if (this.visible) this.repaint();
  5085. };
  5086. /**
  5087. * Unselect current item
  5088. */
  5089. Item.prototype.unselect = function unselect() {
  5090. this.selected = false;
  5091. if (this.visible) this.repaint();
  5092. };
  5093. /**
  5094. * Show the Item in the DOM (when not already visible)
  5095. * @return {Boolean} changed
  5096. */
  5097. Item.prototype.show = function show() {
  5098. return false;
  5099. };
  5100. /**
  5101. * Hide the Item from the DOM (when visible)
  5102. * @return {Boolean} changed
  5103. */
  5104. Item.prototype.hide = function hide() {
  5105. return false;
  5106. };
  5107. /**
  5108. * Repaint the item
  5109. * @return {Boolean} changed
  5110. */
  5111. Item.prototype.repaint = function repaint() {
  5112. // should be implemented by the item
  5113. return false;
  5114. };
  5115. /**
  5116. * Reflow the item
  5117. * @return {Boolean} resized
  5118. */
  5119. Item.prototype.reflow = function reflow() {
  5120. // should be implemented by the item
  5121. return false;
  5122. };
  5123. /**
  5124. * Return the items width
  5125. * @return {Integer} width
  5126. */
  5127. Item.prototype.getWidth = function getWidth() {
  5128. return this.width;
  5129. }
  5130. /**
  5131. * @constructor ItemBox
  5132. * @extends Item
  5133. * @param {ItemSet} parent
  5134. * @param {Object} data Object containing parameters start
  5135. * content, className.
  5136. * @param {Object} [options] Options to set initial property values
  5137. * @param {Object} [defaultOptions] default options
  5138. * // TODO: describe available options
  5139. */
  5140. function ItemBox (parent, data, options, defaultOptions) {
  5141. this.props = {
  5142. dot: {
  5143. left: 0,
  5144. top: 0,
  5145. width: 0,
  5146. height: 0
  5147. },
  5148. line: {
  5149. top: 0,
  5150. left: 0,
  5151. width: 0,
  5152. height: 0
  5153. }
  5154. };
  5155. Item.call(this, parent, data, options, defaultOptions);
  5156. }
  5157. ItemBox.prototype = new Item (null, null);
  5158. /**
  5159. * Repaint the item
  5160. * @return {Boolean} changed
  5161. */
  5162. ItemBox.prototype.repaint = function repaint() {
  5163. // TODO: make an efficient repaint
  5164. var changed = false;
  5165. var dom = this.dom;
  5166. if (!dom) {
  5167. this._create();
  5168. dom = this.dom;
  5169. changed = true;
  5170. }
  5171. if (dom) {
  5172. if (!this.parent) {
  5173. throw new Error('Cannot repaint item: no parent attached');
  5174. }
  5175. if (!dom.box.parentNode) {
  5176. var foreground = this.parent.getForeground();
  5177. if (!foreground) {
  5178. throw new Error('Cannot repaint time axis: ' +
  5179. 'parent has no foreground container element');
  5180. }
  5181. foreground.appendChild(dom.box);
  5182. changed = true;
  5183. }
  5184. if (!dom.line.parentNode) {
  5185. var background = this.parent.getBackground();
  5186. if (!background) {
  5187. throw new Error('Cannot repaint time axis: ' +
  5188. 'parent has no background container element');
  5189. }
  5190. background.appendChild(dom.line);
  5191. changed = true;
  5192. }
  5193. if (!dom.dot.parentNode) {
  5194. var axis = this.parent.getAxis();
  5195. if (!background) {
  5196. throw new Error('Cannot repaint time axis: ' +
  5197. 'parent has no axis container element');
  5198. }
  5199. axis.appendChild(dom.dot);
  5200. changed = true;
  5201. }
  5202. // update contents
  5203. if (this.data.content != this.content) {
  5204. this.content = this.data.content;
  5205. if (this.content instanceof Element) {
  5206. dom.content.innerHTML = '';
  5207. dom.content.appendChild(this.content);
  5208. }
  5209. else if (this.data.content != undefined) {
  5210. dom.content.innerHTML = this.content;
  5211. }
  5212. else {
  5213. throw new Error('Property "content" missing in item ' + this.data.id);
  5214. }
  5215. changed = true;
  5216. }
  5217. // update class
  5218. var className = (this.data.className? ' ' + this.data.className : '') +
  5219. (this.selected ? ' selected' : '');
  5220. if (this.className != className) {
  5221. this.className = className;
  5222. dom.box.className = 'item box' + className;
  5223. dom.line.className = 'item line' + className;
  5224. dom.dot.className = 'item dot' + className;
  5225. changed = true;
  5226. }
  5227. }
  5228. return changed;
  5229. };
  5230. /**
  5231. * Show the item in the DOM (when not already visible). The items DOM will
  5232. * be created when needed.
  5233. * @return {Boolean} changed
  5234. */
  5235. ItemBox.prototype.show = function show() {
  5236. if (!this.dom || !this.dom.box.parentNode) {
  5237. return this.repaint();
  5238. }
  5239. else {
  5240. return false;
  5241. }
  5242. };
  5243. /**
  5244. * Hide the item from the DOM (when visible)
  5245. * @return {Boolean} changed
  5246. */
  5247. ItemBox.prototype.hide = function hide() {
  5248. var changed = false,
  5249. dom = this.dom;
  5250. if (dom) {
  5251. if (dom.box.parentNode) {
  5252. dom.box.parentNode.removeChild(dom.box);
  5253. changed = true;
  5254. }
  5255. if (dom.line.parentNode) {
  5256. dom.line.parentNode.removeChild(dom.line);
  5257. }
  5258. if (dom.dot.parentNode) {
  5259. dom.dot.parentNode.removeChild(dom.dot);
  5260. }
  5261. }
  5262. return changed;
  5263. };
  5264. /**
  5265. * Reflow the item: calculate its actual size and position from the DOM
  5266. * @return {boolean} resized returns true if the axis is resized
  5267. * @override
  5268. */
  5269. ItemBox.prototype.reflow = function reflow() {
  5270. var changed = 0,
  5271. update,
  5272. dom,
  5273. props,
  5274. options,
  5275. margin,
  5276. start,
  5277. align,
  5278. orientation,
  5279. top,
  5280. left,
  5281. data,
  5282. range;
  5283. if (this.data.start == undefined) {
  5284. throw new Error('Property "start" missing in item ' + this.data.id);
  5285. }
  5286. data = this.data;
  5287. range = this.parent && this.parent.range;
  5288. if (data && range) {
  5289. // TODO: account for the width of the item
  5290. var interval = (range.end - range.start);
  5291. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  5292. }
  5293. else {
  5294. this.visible = false;
  5295. }
  5296. if (this.visible) {
  5297. dom = this.dom;
  5298. if (dom) {
  5299. update = util.updateProperty;
  5300. props = this.props;
  5301. options = this.options;
  5302. start = this.parent.toScreen(this.data.start);
  5303. align = options.align || this.defaultOptions.align;
  5304. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5305. orientation = options.orientation || this.defaultOptions.orientation;
  5306. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5307. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5308. changed += update(props.line, 'width', dom.line.offsetWidth);
  5309. changed += update(props.line, 'height', dom.line.offsetHeight);
  5310. changed += update(props.line, 'top', dom.line.offsetTop);
  5311. changed += update(this, 'width', dom.box.offsetWidth);
  5312. changed += update(this, 'height', dom.box.offsetHeight);
  5313. if (align == 'right') {
  5314. left = start - this.width;
  5315. }
  5316. else if (align == 'left') {
  5317. left = start;
  5318. }
  5319. else {
  5320. // default or 'center'
  5321. left = start - this.width / 2;
  5322. }
  5323. changed += update(this, 'left', left);
  5324. changed += update(props.line, 'left', start - props.line.width / 2);
  5325. changed += update(props.dot, 'left', start - props.dot.width / 2);
  5326. changed += update(props.dot, 'top', -props.dot.height / 2);
  5327. if (orientation == 'top') {
  5328. top = margin;
  5329. changed += update(this, 'top', top);
  5330. }
  5331. else {
  5332. // default or 'bottom'
  5333. var parentHeight = this.parent.height;
  5334. top = parentHeight - this.height - margin;
  5335. changed += update(this, 'top', top);
  5336. }
  5337. }
  5338. else {
  5339. changed += 1;
  5340. }
  5341. }
  5342. return (changed > 0);
  5343. };
  5344. /**
  5345. * Create an items DOM
  5346. * @private
  5347. */
  5348. ItemBox.prototype._create = function _create() {
  5349. var dom = this.dom;
  5350. if (!dom) {
  5351. this.dom = dom = {};
  5352. // create the box
  5353. dom.box = document.createElement('DIV');
  5354. // className is updated in repaint()
  5355. // contents box (inside the background box). used for making margins
  5356. dom.content = document.createElement('DIV');
  5357. dom.content.className = 'content';
  5358. dom.box.appendChild(dom.content);
  5359. // line to axis
  5360. dom.line = document.createElement('DIV');
  5361. dom.line.className = 'line';
  5362. // dot on axis
  5363. dom.dot = document.createElement('DIV');
  5364. dom.dot.className = 'dot';
  5365. }
  5366. };
  5367. /**
  5368. * Reposition the item, recalculate its left, top, and width, using the current
  5369. * range and size of the items itemset
  5370. * @override
  5371. */
  5372. ItemBox.prototype.reposition = function reposition() {
  5373. var dom = this.dom,
  5374. props = this.props,
  5375. orientation = this.options.orientation || this.defaultOptions.orientation;
  5376. if (dom) {
  5377. var box = dom.box,
  5378. line = dom.line,
  5379. dot = dom.dot;
  5380. box.style.left = this.left + 'px';
  5381. box.style.top = this.top + 'px';
  5382. line.style.left = props.line.left + 'px';
  5383. if (orientation == 'top') {
  5384. line.style.top = 0 + 'px';
  5385. line.style.height = this.top + 'px';
  5386. }
  5387. else {
  5388. // orientation 'bottom'
  5389. line.style.top = (this.top + this.height) + 'px';
  5390. line.style.height = Math.max(this.parent.height - this.top - this.height +
  5391. this.props.dot.height / 2, 0) + 'px';
  5392. }
  5393. dot.style.left = props.dot.left + 'px';
  5394. dot.style.top = props.dot.top + 'px';
  5395. }
  5396. };
  5397. /**
  5398. * @constructor ItemPoint
  5399. * @extends Item
  5400. * @param {ItemSet} parent
  5401. * @param {Object} data Object containing parameters start
  5402. * content, className.
  5403. * @param {Object} [options] Options to set initial property values
  5404. * @param {Object} [defaultOptions] default options
  5405. * // TODO: describe available options
  5406. */
  5407. function ItemPoint (parent, data, options, defaultOptions) {
  5408. this.props = {
  5409. dot: {
  5410. top: 0,
  5411. width: 0,
  5412. height: 0
  5413. },
  5414. content: {
  5415. height: 0,
  5416. marginLeft: 0
  5417. }
  5418. };
  5419. Item.call(this, parent, data, options, defaultOptions);
  5420. }
  5421. ItemPoint.prototype = new Item (null, null);
  5422. /**
  5423. * Repaint the item
  5424. * @return {Boolean} changed
  5425. */
  5426. ItemPoint.prototype.repaint = function repaint() {
  5427. // TODO: make an efficient repaint
  5428. var changed = false;
  5429. var dom = this.dom;
  5430. if (!dom) {
  5431. this._create();
  5432. dom = this.dom;
  5433. changed = true;
  5434. }
  5435. if (dom) {
  5436. if (!this.parent) {
  5437. throw new Error('Cannot repaint item: no parent attached');
  5438. }
  5439. var foreground = this.parent.getForeground();
  5440. if (!foreground) {
  5441. throw new Error('Cannot repaint time axis: ' +
  5442. 'parent has no foreground container element');
  5443. }
  5444. if (!dom.point.parentNode) {
  5445. foreground.appendChild(dom.point);
  5446. foreground.appendChild(dom.point);
  5447. changed = true;
  5448. }
  5449. // update contents
  5450. if (this.data.content != this.content) {
  5451. this.content = this.data.content;
  5452. if (this.content instanceof Element) {
  5453. dom.content.innerHTML = '';
  5454. dom.content.appendChild(this.content);
  5455. }
  5456. else if (this.data.content != undefined) {
  5457. dom.content.innerHTML = this.content;
  5458. }
  5459. else {
  5460. throw new Error('Property "content" missing in item ' + this.data.id);
  5461. }
  5462. changed = true;
  5463. }
  5464. // update class
  5465. var className = (this.data.className? ' ' + this.data.className : '') +
  5466. (this.selected ? ' selected' : '');
  5467. if (this.className != className) {
  5468. this.className = className;
  5469. dom.point.className = 'item point' + className;
  5470. changed = true;
  5471. }
  5472. }
  5473. return changed;
  5474. };
  5475. /**
  5476. * Show the item in the DOM (when not already visible). The items DOM will
  5477. * be created when needed.
  5478. * @return {Boolean} changed
  5479. */
  5480. ItemPoint.prototype.show = function show() {
  5481. if (!this.dom || !this.dom.point.parentNode) {
  5482. return this.repaint();
  5483. }
  5484. else {
  5485. return false;
  5486. }
  5487. };
  5488. /**
  5489. * Hide the item from the DOM (when visible)
  5490. * @return {Boolean} changed
  5491. */
  5492. ItemPoint.prototype.hide = function hide() {
  5493. var changed = false,
  5494. dom = this.dom;
  5495. if (dom) {
  5496. if (dom.point.parentNode) {
  5497. dom.point.parentNode.removeChild(dom.point);
  5498. changed = true;
  5499. }
  5500. }
  5501. return changed;
  5502. };
  5503. /**
  5504. * Reflow the item: calculate its actual size from the DOM
  5505. * @return {boolean} resized returns true if the axis is resized
  5506. * @override
  5507. */
  5508. ItemPoint.prototype.reflow = function reflow() {
  5509. var changed = 0,
  5510. update,
  5511. dom,
  5512. props,
  5513. options,
  5514. margin,
  5515. orientation,
  5516. start,
  5517. top,
  5518. data,
  5519. range;
  5520. if (this.data.start == undefined) {
  5521. throw new Error('Property "start" missing in item ' + this.data.id);
  5522. }
  5523. data = this.data;
  5524. range = this.parent && this.parent.range;
  5525. if (data && range) {
  5526. // TODO: account for the width of the item
  5527. var interval = (range.end - range.start);
  5528. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  5529. }
  5530. else {
  5531. this.visible = false;
  5532. }
  5533. if (this.visible) {
  5534. dom = this.dom;
  5535. if (dom) {
  5536. update = util.updateProperty;
  5537. props = this.props;
  5538. options = this.options;
  5539. orientation = options.orientation || this.defaultOptions.orientation;
  5540. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5541. start = this.parent.toScreen(this.data.start);
  5542. changed += update(this, 'width', dom.point.offsetWidth);
  5543. changed += update(this, 'height', dom.point.offsetHeight);
  5544. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  5545. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  5546. changed += update(props.content, 'height', dom.content.offsetHeight);
  5547. if (orientation == 'top') {
  5548. top = margin;
  5549. }
  5550. else {
  5551. // default or 'bottom'
  5552. var parentHeight = this.parent.height;
  5553. top = Math.max(parentHeight - this.height - margin, 0);
  5554. }
  5555. changed += update(this, 'top', top);
  5556. changed += update(this, 'left', start - props.dot.width / 2);
  5557. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  5558. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  5559. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  5560. }
  5561. else {
  5562. changed += 1;
  5563. }
  5564. }
  5565. return (changed > 0);
  5566. };
  5567. /**
  5568. * Create an items DOM
  5569. * @private
  5570. */
  5571. ItemPoint.prototype._create = function _create() {
  5572. var dom = this.dom;
  5573. if (!dom) {
  5574. this.dom = dom = {};
  5575. // background box
  5576. dom.point = document.createElement('div');
  5577. // className is updated in repaint()
  5578. // contents box, right from the dot
  5579. dom.content = document.createElement('div');
  5580. dom.content.className = 'content';
  5581. dom.point.appendChild(dom.content);
  5582. // dot at start
  5583. dom.dot = document.createElement('div');
  5584. dom.dot.className = 'dot';
  5585. dom.point.appendChild(dom.dot);
  5586. }
  5587. };
  5588. /**
  5589. * Reposition the item, recalculate its left, top, and width, using the current
  5590. * range and size of the items itemset
  5591. * @override
  5592. */
  5593. ItemPoint.prototype.reposition = function reposition() {
  5594. var dom = this.dom,
  5595. props = this.props;
  5596. if (dom) {
  5597. dom.point.style.top = this.top + 'px';
  5598. dom.point.style.left = this.left + 'px';
  5599. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  5600. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  5601. dom.dot.style.top = props.dot.top + 'px';
  5602. }
  5603. };
  5604. /**
  5605. * @constructor ItemRange
  5606. * @extends Item
  5607. * @param {ItemSet} parent
  5608. * @param {Object} data Object containing parameters start, end
  5609. * content, className.
  5610. * @param {Object} [options] Options to set initial property values
  5611. * @param {Object} [defaultOptions] default options
  5612. * // TODO: describe available options
  5613. */
  5614. function ItemRange (parent, data, options, defaultOptions) {
  5615. this.props = {
  5616. content: {
  5617. left: 0,
  5618. width: 0
  5619. }
  5620. };
  5621. Item.call(this, parent, data, options, defaultOptions);
  5622. }
  5623. ItemRange.prototype = new Item (null, null);
  5624. /**
  5625. * Repaint the item
  5626. * @return {Boolean} changed
  5627. */
  5628. ItemRange.prototype.repaint = function repaint() {
  5629. // TODO: make an efficient repaint
  5630. var changed = false;
  5631. var dom = this.dom;
  5632. if (!dom) {
  5633. this._create();
  5634. dom = this.dom;
  5635. changed = true;
  5636. }
  5637. if (dom) {
  5638. if (!this.parent) {
  5639. throw new Error('Cannot repaint item: no parent attached');
  5640. }
  5641. var foreground = this.parent.getForeground();
  5642. if (!foreground) {
  5643. throw new Error('Cannot repaint time axis: ' +
  5644. 'parent has no foreground container element');
  5645. }
  5646. if (!dom.box.parentNode) {
  5647. foreground.appendChild(dom.box);
  5648. changed = true;
  5649. }
  5650. // update content
  5651. if (this.data.content != this.content) {
  5652. this.content = this.data.content;
  5653. if (this.content instanceof Element) {
  5654. dom.content.innerHTML = '';
  5655. dom.content.appendChild(this.content);
  5656. }
  5657. else if (this.data.content != undefined) {
  5658. dom.content.innerHTML = this.content;
  5659. }
  5660. else {
  5661. throw new Error('Property "content" missing in item ' + this.data.id);
  5662. }
  5663. changed = true;
  5664. }
  5665. // update class
  5666. var className = (this.data.className? ' ' + this.data.className : '') +
  5667. (this.selected ? ' selected' : '');
  5668. if (this.className != className) {
  5669. this.className = className;
  5670. dom.box.className = 'item range' + className;
  5671. changed = true;
  5672. }
  5673. }
  5674. return changed;
  5675. };
  5676. /**
  5677. * Show the item in the DOM (when not already visible). The items DOM will
  5678. * be created when needed.
  5679. * @return {Boolean} changed
  5680. */
  5681. ItemRange.prototype.show = function show() {
  5682. if (!this.dom || !this.dom.box.parentNode) {
  5683. return this.repaint();
  5684. }
  5685. else {
  5686. return false;
  5687. }
  5688. };
  5689. /**
  5690. * Hide the item from the DOM (when visible)
  5691. * @return {Boolean} changed
  5692. */
  5693. ItemRange.prototype.hide = function hide() {
  5694. var changed = false,
  5695. dom = this.dom;
  5696. if (dom) {
  5697. if (dom.box.parentNode) {
  5698. dom.box.parentNode.removeChild(dom.box);
  5699. changed = true;
  5700. }
  5701. }
  5702. return changed;
  5703. };
  5704. /**
  5705. * Reflow the item: calculate its actual size from the DOM
  5706. * @return {boolean} resized returns true if the axis is resized
  5707. * @override
  5708. */
  5709. ItemRange.prototype.reflow = function reflow() {
  5710. var changed = 0,
  5711. dom,
  5712. props,
  5713. options,
  5714. margin,
  5715. padding,
  5716. parent,
  5717. start,
  5718. end,
  5719. data,
  5720. range,
  5721. update,
  5722. box,
  5723. parentWidth,
  5724. contentLeft,
  5725. orientation,
  5726. top;
  5727. if (this.data.start == undefined) {
  5728. throw new Error('Property "start" missing in item ' + this.data.id);
  5729. }
  5730. if (this.data.end == undefined) {
  5731. throw new Error('Property "end" missing in item ' + this.data.id);
  5732. }
  5733. data = this.data;
  5734. range = this.parent && this.parent.range;
  5735. if (data && range) {
  5736. // TODO: account for the width of the item. Take some margin
  5737. this.visible = (data.start < range.end) && (data.end > range.start);
  5738. }
  5739. else {
  5740. this.visible = false;
  5741. }
  5742. if (this.visible) {
  5743. dom = this.dom;
  5744. if (dom) {
  5745. props = this.props;
  5746. options = this.options;
  5747. parent = this.parent;
  5748. start = parent.toScreen(this.data.start);
  5749. end = parent.toScreen(this.data.end);
  5750. update = util.updateProperty;
  5751. box = dom.box;
  5752. parentWidth = parent.width;
  5753. orientation = options.orientation || this.defaultOptions.orientation;
  5754. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  5755. padding = options.padding || this.defaultOptions.padding;
  5756. changed += update(props.content, 'width', dom.content.offsetWidth);
  5757. changed += update(this, 'height', box.offsetHeight);
  5758. // limit the width of the this, as browsers cannot draw very wide divs
  5759. if (start < -parentWidth) {
  5760. start = -parentWidth;
  5761. }
  5762. if (end > 2 * parentWidth) {
  5763. end = 2 * parentWidth;
  5764. }
  5765. // when range exceeds left of the window, position the contents at the left of the visible area
  5766. if (start < 0) {
  5767. contentLeft = Math.min(-start,
  5768. (end - start - props.content.width - 2 * padding));
  5769. // TODO: remove the need for options.padding. it's terrible.
  5770. }
  5771. else {
  5772. contentLeft = 0;
  5773. }
  5774. changed += update(props.content, 'left', contentLeft);
  5775. if (orientation == 'top') {
  5776. top = margin;
  5777. changed += update(this, 'top', top);
  5778. }
  5779. else {
  5780. // default or 'bottom'
  5781. top = parent.height - this.height - margin;
  5782. changed += update(this, 'top', top);
  5783. }
  5784. changed += update(this, 'left', start);
  5785. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  5786. }
  5787. else {
  5788. changed += 1;
  5789. }
  5790. }
  5791. return (changed > 0);
  5792. };
  5793. /**
  5794. * Create an items DOM
  5795. * @private
  5796. */
  5797. ItemRange.prototype._create = function _create() {
  5798. var dom = this.dom;
  5799. if (!dom) {
  5800. this.dom = dom = {};
  5801. // background box
  5802. dom.box = document.createElement('div');
  5803. // className is updated in repaint()
  5804. // contents box
  5805. dom.content = document.createElement('div');
  5806. dom.content.className = 'content';
  5807. dom.box.appendChild(dom.content);
  5808. }
  5809. };
  5810. /**
  5811. * Reposition the item, recalculate its left, top, and width, using the current
  5812. * range and size of the items itemset
  5813. * @override
  5814. */
  5815. ItemRange.prototype.reposition = function reposition() {
  5816. var dom = this.dom,
  5817. props = this.props;
  5818. if (dom) {
  5819. dom.box.style.top = this.top + 'px';
  5820. dom.box.style.left = this.left + 'px';
  5821. dom.box.style.width = this.width + 'px';
  5822. dom.content.style.left = props.content.left + 'px';
  5823. }
  5824. };
  5825. /**
  5826. * @constructor ItemRangeOverflow
  5827. * @extends ItemRange
  5828. * @param {ItemSet} parent
  5829. * @param {Object} data Object containing parameters start, end
  5830. * content, className.
  5831. * @param {Object} [options] Options to set initial property values
  5832. * @param {Object} [defaultOptions] default options
  5833. * // TODO: describe available options
  5834. */
  5835. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  5836. this.props = {
  5837. content: {
  5838. left: 0,
  5839. width: 0
  5840. }
  5841. };
  5842. ItemRange.call(this, parent, data, options, defaultOptions);
  5843. }
  5844. ItemRangeOverflow.prototype = new ItemRange (null, null);
  5845. /**
  5846. * Repaint the item
  5847. * @return {Boolean} changed
  5848. */
  5849. ItemRangeOverflow.prototype.repaint = function repaint() {
  5850. // TODO: make an efficient repaint
  5851. var changed = false;
  5852. var dom = this.dom;
  5853. if (!dom) {
  5854. this._create();
  5855. dom = this.dom;
  5856. changed = true;
  5857. }
  5858. if (dom) {
  5859. if (!this.parent) {
  5860. throw new Error('Cannot repaint item: no parent attached');
  5861. }
  5862. var foreground = this.parent.getForeground();
  5863. if (!foreground) {
  5864. throw new Error('Cannot repaint time axis: ' +
  5865. 'parent has no foreground container element');
  5866. }
  5867. if (!dom.box.parentNode) {
  5868. foreground.appendChild(dom.box);
  5869. changed = true;
  5870. }
  5871. // update content
  5872. if (this.data.content != this.content) {
  5873. this.content = this.data.content;
  5874. if (this.content instanceof Element) {
  5875. dom.content.innerHTML = '';
  5876. dom.content.appendChild(this.content);
  5877. }
  5878. else if (this.data.content != undefined) {
  5879. dom.content.innerHTML = this.content;
  5880. }
  5881. else {
  5882. throw new Error('Property "content" missing in item ' + this.data.id);
  5883. }
  5884. changed = true;
  5885. }
  5886. // update class
  5887. var className = this.data.className ? (' ' + this.data.className) : '';
  5888. if (this.className != className) {
  5889. this.className = className;
  5890. dom.box.className = 'item rangeoverflow' + className;
  5891. changed = true;
  5892. }
  5893. }
  5894. return changed;
  5895. };
  5896. /**
  5897. * Return the items width
  5898. * @return {Number} width
  5899. */
  5900. ItemRangeOverflow.prototype.getWidth = function getWidth() {
  5901. if (this.props.content !== undefined && this.width < this.props.content.width)
  5902. return this.props.content.width;
  5903. else
  5904. return this.width;
  5905. };
  5906. /**
  5907. * @constructor Group
  5908. * @param {GroupSet} parent
  5909. * @param {Number | String} groupId
  5910. * @param {Object} [options] Options to set initial property values
  5911. * // TODO: describe available options
  5912. * @extends Component
  5913. */
  5914. function Group (parent, groupId, options) {
  5915. this.id = util.randomUUID();
  5916. this.parent = parent;
  5917. this.groupId = groupId;
  5918. this.itemset = null; // ItemSet
  5919. this.options = options || {};
  5920. this.options.top = 0;
  5921. this.props = {
  5922. label: {
  5923. width: 0,
  5924. height: 0
  5925. }
  5926. };
  5927. this.top = 0;
  5928. this.left = 0;
  5929. this.width = 0;
  5930. this.height = 0;
  5931. }
  5932. Group.prototype = new Component();
  5933. // TODO: comment
  5934. Group.prototype.setOptions = Component.prototype.setOptions;
  5935. /**
  5936. * Get the container element of the panel, which can be used by a child to
  5937. * add its own widgets.
  5938. * @returns {HTMLElement} container
  5939. */
  5940. Group.prototype.getContainer = function () {
  5941. return this.parent.getContainer();
  5942. };
  5943. /**
  5944. * Set item set for the group. The group will create a view on the itemset,
  5945. * filtered by the groups id.
  5946. * @param {DataSet | DataView} items
  5947. */
  5948. Group.prototype.setItems = function setItems(items) {
  5949. if (this.itemset) {
  5950. // remove current item set
  5951. this.itemset.hide();
  5952. this.itemset.setItems();
  5953. this.parent.controller.remove(this.itemset);
  5954. this.itemset = null;
  5955. }
  5956. if (items) {
  5957. var groupId = this.groupId;
  5958. var itemsetOptions = Object.create(this.options);
  5959. this.itemset = new ItemSet(this, null, itemsetOptions);
  5960. this.itemset.setRange(this.parent.range);
  5961. this.view = new DataView(items, {
  5962. filter: function (item) {
  5963. return item.group == groupId;
  5964. }
  5965. });
  5966. this.itemset.setItems(this.view);
  5967. this.parent.controller.add(this.itemset);
  5968. }
  5969. };
  5970. /**
  5971. * Change the item selection, and/or get currently selected items
  5972. * @param {Array} [ids] An array with zero or more ids of the items to be selected.
  5973. * @return {Array} ids The ids of the selected items
  5974. */
  5975. Group.prototype.select = function select(ids) {
  5976. return this.itemset ? this.itemset.select(ids) : [];
  5977. };
  5978. /**
  5979. * Repaint the item
  5980. * @return {Boolean} changed
  5981. */
  5982. Group.prototype.repaint = function repaint() {
  5983. return false;
  5984. };
  5985. /**
  5986. * Reflow the item
  5987. * @return {Boolean} resized
  5988. */
  5989. Group.prototype.reflow = function reflow() {
  5990. var changed = 0,
  5991. update = util.updateProperty;
  5992. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  5993. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  5994. // TODO: reckon with the height of the group label
  5995. if (this.label) {
  5996. var inner = this.label.firstChild;
  5997. changed += update(this.props.label, 'width', inner.clientWidth);
  5998. changed += update(this.props.label, 'height', inner.clientHeight);
  5999. }
  6000. else {
  6001. changed += update(this.props.label, 'width', 0);
  6002. changed += update(this.props.label, 'height', 0);
  6003. }
  6004. return (changed > 0);
  6005. };
  6006. /**
  6007. * An GroupSet holds a set of groups
  6008. * @param {Component} parent
  6009. * @param {Component[]} [depends] Components on which this components depends
  6010. * (except for the parent)
  6011. * @param {Object} [options] See GroupSet.setOptions for the available
  6012. * options.
  6013. * @constructor GroupSet
  6014. * @extends Panel
  6015. */
  6016. function GroupSet(parent, depends, options) {
  6017. this.id = util.randomUUID();
  6018. this.parent = parent;
  6019. this.depends = depends;
  6020. this.options = options || {};
  6021. this.range = null; // Range or Object {start: number, end: number}
  6022. this.itemsData = null; // DataSet with items
  6023. this.groupsData = null; // DataSet with groups
  6024. this.groups = {}; // map with groups
  6025. this.dom = {};
  6026. this.props = {
  6027. labels: {
  6028. width: 0
  6029. }
  6030. };
  6031. // TODO: implement right orientation of the labels
  6032. // changes in groups are queued key/value map containing id/action
  6033. this.queue = {};
  6034. var me = this;
  6035. this.listeners = {
  6036. 'add': function (event, params) {
  6037. me._onAdd(params.items);
  6038. },
  6039. 'update': function (event, params) {
  6040. me._onUpdate(params.items);
  6041. },
  6042. 'remove': function (event, params) {
  6043. me._onRemove(params.items);
  6044. }
  6045. };
  6046. }
  6047. GroupSet.prototype = new Panel();
  6048. /**
  6049. * Set options for the GroupSet. Existing options will be extended/overwritten.
  6050. * @param {Object} [options] The following options are available:
  6051. * {String | function} groupsOrder
  6052. * TODO: describe options
  6053. */
  6054. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  6055. GroupSet.prototype.setRange = function (range) {
  6056. // TODO: implement setRange
  6057. };
  6058. /**
  6059. * Set items
  6060. * @param {vis.DataSet | null} items
  6061. */
  6062. GroupSet.prototype.setItems = function setItems(items) {
  6063. this.itemsData = items;
  6064. for (var id in this.groups) {
  6065. if (this.groups.hasOwnProperty(id)) {
  6066. var group = this.groups[id];
  6067. group.setItems(items);
  6068. }
  6069. }
  6070. };
  6071. /**
  6072. * Get items
  6073. * @return {vis.DataSet | null} items
  6074. */
  6075. GroupSet.prototype.getItems = function getItems() {
  6076. return this.itemsData;
  6077. };
  6078. /**
  6079. * Set range (start and end).
  6080. * @param {Range | Object} range A Range or an object containing start and end.
  6081. */
  6082. GroupSet.prototype.setRange = function setRange(range) {
  6083. this.range = range;
  6084. };
  6085. /**
  6086. * Set groups
  6087. * @param {vis.DataSet} groups
  6088. */
  6089. GroupSet.prototype.setGroups = function setGroups(groups) {
  6090. var me = this,
  6091. ids;
  6092. // unsubscribe from current dataset
  6093. if (this.groupsData) {
  6094. util.forEach(this.listeners, function (callback, event) {
  6095. me.groupsData.unsubscribe(event, callback);
  6096. });
  6097. // remove all drawn groups
  6098. ids = this.groupsData.getIds();
  6099. this._onRemove(ids);
  6100. }
  6101. // replace the dataset
  6102. if (!groups) {
  6103. this.groupsData = null;
  6104. }
  6105. else if (groups instanceof DataSet) {
  6106. this.groupsData = groups;
  6107. }
  6108. else {
  6109. this.groupsData = new DataSet({
  6110. convert: {
  6111. start: 'Date',
  6112. end: 'Date'
  6113. }
  6114. });
  6115. this.groupsData.add(groups);
  6116. }
  6117. if (this.groupsData) {
  6118. // subscribe to new dataset
  6119. var id = this.id;
  6120. util.forEach(this.listeners, function (callback, event) {
  6121. me.groupsData.subscribe(event, callback, id);
  6122. });
  6123. // draw all new groups
  6124. ids = this.groupsData.getIds();
  6125. this._onAdd(ids);
  6126. }
  6127. };
  6128. /**
  6129. * Get groups
  6130. * @return {vis.DataSet | null} groups
  6131. */
  6132. GroupSet.prototype.getGroups = function getGroups() {
  6133. return this.groupsData;
  6134. };
  6135. /**
  6136. * Change the item selection, and/or get currently selected items
  6137. * @param {Array} [ids] An array with zero or more ids of the items to be selected.
  6138. * @return {Array} ids The ids of the selected items
  6139. */
  6140. GroupSet.prototype.select = function select(ids) {
  6141. var selection = [],
  6142. groups = this.groups;
  6143. // iterate over each of the groups
  6144. for (var id in groups) {
  6145. if (groups.hasOwnProperty(id)) {
  6146. var group = groups[id];
  6147. selection = selection.concat(group.select(ids));
  6148. }
  6149. }
  6150. return selection;
  6151. };
  6152. /**
  6153. * Repaint the component
  6154. * @return {Boolean} changed
  6155. */
  6156. GroupSet.prototype.repaint = function repaint() {
  6157. var changed = 0,
  6158. i, id, group, label,
  6159. update = util.updateProperty,
  6160. asSize = util.option.asSize,
  6161. asElement = util.option.asElement,
  6162. options = this.options,
  6163. frame = this.dom.frame,
  6164. labels = this.dom.labels,
  6165. labelSet = this.dom.labelSet;
  6166. // create frame
  6167. if (!this.parent) {
  6168. throw new Error('Cannot repaint groupset: no parent attached');
  6169. }
  6170. var parentContainer = this.parent.getContainer();
  6171. if (!parentContainer) {
  6172. throw new Error('Cannot repaint groupset: parent has no container element');
  6173. }
  6174. if (!frame) {
  6175. frame = document.createElement('div');
  6176. frame.className = 'groupset';
  6177. this.dom.frame = frame;
  6178. var className = options.className;
  6179. if (className) {
  6180. util.addClassName(frame, util.option.asString(className));
  6181. }
  6182. changed += 1;
  6183. }
  6184. if (!frame.parentNode) {
  6185. parentContainer.appendChild(frame);
  6186. changed += 1;
  6187. }
  6188. // create labels
  6189. var labelContainer = asElement(options.labelContainer);
  6190. if (!labelContainer) {
  6191. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  6192. }
  6193. if (!labels) {
  6194. labels = document.createElement('div');
  6195. labels.className = 'labels';
  6196. this.dom.labels = labels;
  6197. }
  6198. if (!labelSet) {
  6199. labelSet = document.createElement('div');
  6200. labelSet.className = 'label-set';
  6201. labels.appendChild(labelSet);
  6202. this.dom.labelSet = labelSet;
  6203. }
  6204. if (!labels.parentNode || labels.parentNode != labelContainer) {
  6205. if (labels.parentNode) {
  6206. labels.parentNode.removeChild(labels.parentNode);
  6207. }
  6208. labelContainer.appendChild(labels);
  6209. }
  6210. // reposition frame
  6211. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  6212. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6213. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6214. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6215. // reposition labels
  6216. changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
  6217. changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
  6218. var me = this,
  6219. queue = this.queue,
  6220. groups = this.groups,
  6221. groupsData = this.groupsData;
  6222. // show/hide added/changed/removed groups
  6223. var ids = Object.keys(queue);
  6224. if (ids.length) {
  6225. ids.forEach(function (id) {
  6226. var action = queue[id];
  6227. var group = groups[id];
  6228. //noinspection FallthroughInSwitchStatementJS
  6229. switch (action) {
  6230. case 'add':
  6231. case 'update':
  6232. if (!group) {
  6233. var groupOptions = Object.create(me.options);
  6234. util.extend(groupOptions, {
  6235. height: null,
  6236. maxHeight: null
  6237. });
  6238. group = new Group(me, id, groupOptions);
  6239. group.setItems(me.itemsData); // attach items data
  6240. groups[id] = group;
  6241. me.controller.add(group);
  6242. }
  6243. // TODO: update group data
  6244. group.data = groupsData.get(id);
  6245. delete queue[id];
  6246. break;
  6247. case 'remove':
  6248. if (group) {
  6249. group.setItems(); // detach items data
  6250. delete groups[id];
  6251. me.controller.remove(group);
  6252. }
  6253. // update lists
  6254. delete queue[id];
  6255. break;
  6256. default:
  6257. console.log('Error: unknown action "' + action + '"');
  6258. }
  6259. });
  6260. // the groupset depends on each of the groups
  6261. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  6262. // TODO: apply dependencies of the groupset
  6263. // update the top positions of the groups in the correct order
  6264. var orderedGroups = this.groupsData.getIds({
  6265. order: this.options.groupOrder
  6266. });
  6267. for (i = 0; i < orderedGroups.length; i++) {
  6268. (function (group, prevGroup) {
  6269. var top = 0;
  6270. if (prevGroup) {
  6271. top = function () {
  6272. // TODO: top must reckon with options.maxHeight
  6273. return prevGroup.top + prevGroup.height;
  6274. }
  6275. }
  6276. group.setOptions({
  6277. top: top
  6278. });
  6279. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  6280. }
  6281. // (re)create the labels
  6282. while (labelSet.firstChild) {
  6283. labelSet.removeChild(labelSet.firstChild);
  6284. }
  6285. for (i = 0; i < orderedGroups.length; i++) {
  6286. id = orderedGroups[i];
  6287. label = this._createLabel(id);
  6288. labelSet.appendChild(label);
  6289. }
  6290. changed++;
  6291. }
  6292. // reposition the labels
  6293. // TODO: labels are not displayed correctly when orientation=='top'
  6294. // TODO: width of labelPanel is not immediately updated on a change in groups
  6295. for (id in groups) {
  6296. if (groups.hasOwnProperty(id)) {
  6297. group = groups[id];
  6298. label = group.label;
  6299. if (label) {
  6300. label.style.top = group.top + 'px';
  6301. label.style.height = group.height + 'px';
  6302. }
  6303. }
  6304. }
  6305. return (changed > 0);
  6306. };
  6307. /**
  6308. * Create a label for group with given id
  6309. * @param {Number} id
  6310. * @return {Element} label
  6311. * @private
  6312. */
  6313. GroupSet.prototype._createLabel = function(id) {
  6314. var group = this.groups[id];
  6315. var label = document.createElement('div');
  6316. label.className = 'label';
  6317. var inner = document.createElement('div');
  6318. inner.className = 'inner';
  6319. label.appendChild(inner);
  6320. var content = group.data && group.data.content;
  6321. if (content instanceof Element) {
  6322. inner.appendChild(content);
  6323. }
  6324. else if (content != undefined) {
  6325. inner.innerHTML = content;
  6326. }
  6327. var className = group.data && group.data.className;
  6328. if (className) {
  6329. util.addClassName(label, className);
  6330. }
  6331. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  6332. return label;
  6333. };
  6334. /**
  6335. * Get container element
  6336. * @return {HTMLElement} container
  6337. */
  6338. GroupSet.prototype.getContainer = function getContainer() {
  6339. return this.dom.frame;
  6340. };
  6341. /**
  6342. * Get the width of the group labels
  6343. * @return {Number} width
  6344. */
  6345. GroupSet.prototype.getLabelsWidth = function getContainer() {
  6346. return this.props.labels.width;
  6347. };
  6348. /**
  6349. * Reflow the component
  6350. * @return {Boolean} resized
  6351. */
  6352. GroupSet.prototype.reflow = function reflow() {
  6353. var changed = 0,
  6354. id, group,
  6355. options = this.options,
  6356. update = util.updateProperty,
  6357. asNumber = util.option.asNumber,
  6358. asSize = util.option.asSize,
  6359. frame = this.dom.frame;
  6360. if (frame) {
  6361. var maxHeight = asNumber(options.maxHeight);
  6362. var fixedHeight = (asSize(options.height) != null);
  6363. var height;
  6364. if (fixedHeight) {
  6365. height = frame.offsetHeight;
  6366. }
  6367. else {
  6368. // height is not specified, calculate the sum of the height of all groups
  6369. height = 0;
  6370. for (id in this.groups) {
  6371. if (this.groups.hasOwnProperty(id)) {
  6372. group = this.groups[id];
  6373. height += group.height;
  6374. }
  6375. }
  6376. }
  6377. if (maxHeight != null) {
  6378. height = Math.min(height, maxHeight);
  6379. }
  6380. changed += update(this, 'height', height);
  6381. changed += update(this, 'top', frame.offsetTop);
  6382. changed += update(this, 'left', frame.offsetLeft);
  6383. changed += update(this, 'width', frame.offsetWidth);
  6384. }
  6385. // calculate the maximum width of the labels
  6386. var width = 0;
  6387. for (id in this.groups) {
  6388. if (this.groups.hasOwnProperty(id)) {
  6389. group = this.groups[id];
  6390. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  6391. width = Math.max(width, labelWidth);
  6392. }
  6393. }
  6394. changed += update(this.props.labels, 'width', width);
  6395. return (changed > 0);
  6396. };
  6397. /**
  6398. * Hide the component from the DOM
  6399. * @return {Boolean} changed
  6400. */
  6401. GroupSet.prototype.hide = function hide() {
  6402. if (this.dom.frame && this.dom.frame.parentNode) {
  6403. this.dom.frame.parentNode.removeChild(this.dom.frame);
  6404. return true;
  6405. }
  6406. else {
  6407. return false;
  6408. }
  6409. };
  6410. /**
  6411. * Show the component in the DOM (when not already visible).
  6412. * A repaint will be executed when the component is not visible
  6413. * @return {Boolean} changed
  6414. */
  6415. GroupSet.prototype.show = function show() {
  6416. if (!this.dom.frame || !this.dom.frame.parentNode) {
  6417. return this.repaint();
  6418. }
  6419. else {
  6420. return false;
  6421. }
  6422. };
  6423. /**
  6424. * Handle updated groups
  6425. * @param {Number[]} ids
  6426. * @private
  6427. */
  6428. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  6429. this._toQueue(ids, 'update');
  6430. };
  6431. /**
  6432. * Handle changed groups
  6433. * @param {Number[]} ids
  6434. * @private
  6435. */
  6436. GroupSet.prototype._onAdd = function _onAdd(ids) {
  6437. this._toQueue(ids, 'add');
  6438. };
  6439. /**
  6440. * Handle removed groups
  6441. * @param {Number[]} ids
  6442. * @private
  6443. */
  6444. GroupSet.prototype._onRemove = function _onRemove(ids) {
  6445. this._toQueue(ids, 'remove');
  6446. };
  6447. /**
  6448. * Put groups in the queue to be added/updated/remove
  6449. * @param {Number[]} ids
  6450. * @param {String} action can be 'add', 'update', 'remove'
  6451. */
  6452. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  6453. var queue = this.queue;
  6454. ids.forEach(function (id) {
  6455. queue[id] = action;
  6456. });
  6457. if (this.controller) {
  6458. //this.requestReflow();
  6459. this.requestRepaint();
  6460. }
  6461. };
  6462. /**
  6463. * Create a timeline visualization
  6464. * @param {HTMLElement} container
  6465. * @param {vis.DataSet | Array | DataTable} [items]
  6466. * @param {Object} [options] See Timeline.setOptions for the available options.
  6467. * @constructor
  6468. */
  6469. function Timeline (container, items, options) {
  6470. var me = this;
  6471. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  6472. this.options = {
  6473. orientation: 'bottom',
  6474. min: null,
  6475. max: null,
  6476. zoomMin: 10, // milliseconds
  6477. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  6478. // moveable: true, // TODO: option moveable
  6479. // zoomable: true, // TODO: option zoomable
  6480. showMinorLabels: true,
  6481. showMajorLabels: true,
  6482. showCurrentTime: false,
  6483. showCustomTime: false,
  6484. autoResize: false
  6485. };
  6486. // controller
  6487. this.controller = new Controller();
  6488. // root panel
  6489. if (!container) {
  6490. throw new Error('No container element provided');
  6491. }
  6492. var rootOptions = Object.create(this.options);
  6493. rootOptions.height = function () {
  6494. // TODO: change to height
  6495. if (me.options.height) {
  6496. // fixed height
  6497. return me.options.height;
  6498. }
  6499. else {
  6500. // auto height
  6501. return (me.timeaxis.height + me.content.height) + 'px';
  6502. }
  6503. };
  6504. this.rootPanel = new RootPanel(container, rootOptions);
  6505. this.controller.add(this.rootPanel);
  6506. // item panel
  6507. var itemOptions = Object.create(this.options);
  6508. itemOptions.left = function () {
  6509. return me.labelPanel.width;
  6510. };
  6511. itemOptions.width = function () {
  6512. return me.rootPanel.width - me.labelPanel.width;
  6513. };
  6514. itemOptions.top = null;
  6515. itemOptions.height = null;
  6516. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  6517. this.controller.add(this.itemPanel);
  6518. // label panel
  6519. var labelOptions = Object.create(this.options);
  6520. labelOptions.top = null;
  6521. labelOptions.left = null;
  6522. labelOptions.height = null;
  6523. labelOptions.width = function () {
  6524. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  6525. return me.content.getLabelsWidth();
  6526. }
  6527. else {
  6528. return 0;
  6529. }
  6530. };
  6531. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  6532. this.controller.add(this.labelPanel);
  6533. // range
  6534. var rangeOptions = Object.create(this.options);
  6535. this.range = new Range(rangeOptions);
  6536. this.range.setRange(
  6537. now.clone().add('days', -3).valueOf(),
  6538. now.clone().add('days', 4).valueOf()
  6539. );
  6540. // TODO: reckon with options moveable and zoomable
  6541. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  6542. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  6543. this.range.on('rangechange', function () {
  6544. var force = true;
  6545. me.controller.requestReflow(force);
  6546. });
  6547. this.range.on('rangechanged', function () {
  6548. var force = true;
  6549. me.controller.requestReflow(force);
  6550. });
  6551. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  6552. // time axis
  6553. var timeaxisOptions = Object.create(rootOptions);
  6554. timeaxisOptions.range = this.range;
  6555. timeaxisOptions.left = null;
  6556. timeaxisOptions.top = null;
  6557. timeaxisOptions.width = '100%';
  6558. timeaxisOptions.height = null;
  6559. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  6560. this.timeaxis.setRange(this.range);
  6561. this.controller.add(this.timeaxis);
  6562. // current time bar
  6563. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  6564. this.controller.add(this.currenttime);
  6565. // custom time bar
  6566. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  6567. this.controller.add(this.customtime);
  6568. // create groupset
  6569. this.setGroups(null);
  6570. this.itemsData = null; // DataSet
  6571. this.groupsData = null; // DataSet
  6572. // apply options
  6573. if (options) {
  6574. this.setOptions(options);
  6575. }
  6576. // create itemset and groupset
  6577. if (items) {
  6578. this.setItems(items);
  6579. }
  6580. }
  6581. /**
  6582. * Set options
  6583. * @param {Object} options TODO: describe the available options
  6584. */
  6585. Timeline.prototype.setOptions = function (options) {
  6586. util.extend(this.options, options);
  6587. // force update of range
  6588. // options.start and options.end can be undefined
  6589. //this.range.setRange(options.start, options.end);
  6590. this.range.setRange();
  6591. this.controller.reflow();
  6592. this.controller.repaint();
  6593. };
  6594. /**
  6595. * Set a custom time bar
  6596. * @param {Date} time
  6597. */
  6598. Timeline.prototype.setCustomTime = function (time) {
  6599. this.customtime._setCustomTime(time);
  6600. };
  6601. /**
  6602. * Retrieve the current custom time.
  6603. * @return {Date} customTime
  6604. */
  6605. Timeline.prototype.getCustomTime = function() {
  6606. return new Date(this.customtime.customTime.valueOf());
  6607. };
  6608. /**
  6609. * Set items
  6610. * @param {vis.DataSet | Array | DataTable | null} items
  6611. */
  6612. Timeline.prototype.setItems = function(items) {
  6613. var initialLoad = (this.itemsData == null);
  6614. // convert to type DataSet when needed
  6615. var newItemSet;
  6616. if (!items) {
  6617. newItemSet = null;
  6618. }
  6619. else if (items instanceof DataSet) {
  6620. newItemSet = items;
  6621. }
  6622. if (!(items instanceof DataSet)) {
  6623. newItemSet = new DataSet({
  6624. convert: {
  6625. start: 'Date',
  6626. end: 'Date'
  6627. }
  6628. });
  6629. newItemSet.add(items);
  6630. }
  6631. // set items
  6632. this.itemsData = newItemSet;
  6633. this.content.setItems(newItemSet);
  6634. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  6635. // apply the data range as range
  6636. var dataRange = this.getItemRange();
  6637. // add 5% space on both sides
  6638. var min = dataRange.min;
  6639. var max = dataRange.max;
  6640. if (min != null && max != null) {
  6641. var interval = (max.valueOf() - min.valueOf());
  6642. if (interval <= 0) {
  6643. // prevent an empty interval
  6644. interval = 24 * 60 * 60 * 1000; // 1 day
  6645. }
  6646. min = new Date(min.valueOf() - interval * 0.05);
  6647. max = new Date(max.valueOf() + interval * 0.05);
  6648. }
  6649. // override specified start and/or end date
  6650. if (this.options.start != undefined) {
  6651. min = util.convert(this.options.start, 'Date');
  6652. }
  6653. if (this.options.end != undefined) {
  6654. max = util.convert(this.options.end, 'Date');
  6655. }
  6656. // apply range if there is a min or max available
  6657. if (min != null || max != null) {
  6658. this.range.setRange(min, max);
  6659. }
  6660. }
  6661. };
  6662. /**
  6663. * Set groups
  6664. * @param {vis.DataSet | Array | DataTable} groups
  6665. */
  6666. Timeline.prototype.setGroups = function(groups) {
  6667. var me = this;
  6668. this.groupsData = groups;
  6669. // switch content type between ItemSet or GroupSet when needed
  6670. var Type = this.groupsData ? GroupSet : ItemSet;
  6671. if (!(this.content instanceof Type)) {
  6672. // remove old content set
  6673. if (this.content) {
  6674. this.content.hide();
  6675. if (this.content.setItems) {
  6676. this.content.setItems(); // disconnect from items
  6677. }
  6678. if (this.content.setGroups) {
  6679. this.content.setGroups(); // disconnect from groups
  6680. }
  6681. this.controller.remove(this.content);
  6682. }
  6683. // create new content set
  6684. var options = Object.create(this.options);
  6685. util.extend(options, {
  6686. top: function () {
  6687. if (me.options.orientation == 'top') {
  6688. return me.timeaxis.height;
  6689. }
  6690. else {
  6691. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  6692. }
  6693. },
  6694. left: null,
  6695. width: '100%',
  6696. height: function () {
  6697. if (me.options.height) {
  6698. // fixed height
  6699. return me.itemPanel.height - me.timeaxis.height;
  6700. }
  6701. else {
  6702. // auto height
  6703. return null;
  6704. }
  6705. },
  6706. maxHeight: function () {
  6707. // TODO: change maxHeight to be a css string like '100%' or '300px'
  6708. if (me.options.maxHeight) {
  6709. if (!util.isNumber(me.options.maxHeight)) {
  6710. throw new TypeError('Number expected for property maxHeight');
  6711. }
  6712. return me.options.maxHeight - me.timeaxis.height;
  6713. }
  6714. else {
  6715. return null;
  6716. }
  6717. },
  6718. labelContainer: function () {
  6719. return me.labelPanel.getContainer();
  6720. }
  6721. });
  6722. this.content = new Type(this.itemPanel, [this.timeaxis], options);
  6723. if (this.content.setRange) {
  6724. this.content.setRange(this.range);
  6725. }
  6726. if (this.content.setItems) {
  6727. this.content.setItems(this.itemsData);
  6728. }
  6729. if (this.content.setGroups) {
  6730. this.content.setGroups(this.groupsData);
  6731. }
  6732. this.controller.add(this.content);
  6733. }
  6734. };
  6735. /**
  6736. * Get the data range of the item set.
  6737. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  6738. * When no minimum is found, min==null
  6739. * When no maximum is found, max==null
  6740. */
  6741. Timeline.prototype.getItemRange = function getItemRange() {
  6742. // calculate min from start filed
  6743. var itemsData = this.itemsData,
  6744. min = null,
  6745. max = null;
  6746. if (itemsData) {
  6747. // calculate the minimum value of the field 'start'
  6748. var minItem = itemsData.min('start');
  6749. min = minItem ? minItem.start.valueOf() : null;
  6750. // calculate maximum value of fields 'start' and 'end'
  6751. var maxStartItem = itemsData.max('start');
  6752. if (maxStartItem) {
  6753. max = maxStartItem.start.valueOf();
  6754. }
  6755. var maxEndItem = itemsData.max('end');
  6756. if (maxEndItem) {
  6757. if (max == null) {
  6758. max = maxEndItem.end.valueOf();
  6759. }
  6760. else {
  6761. max = Math.max(max, maxEndItem.end.valueOf());
  6762. }
  6763. }
  6764. }
  6765. return {
  6766. min: (min != null) ? new Date(min) : null,
  6767. max: (max != null) ? new Date(max) : null
  6768. };
  6769. };
  6770. /**
  6771. * Change the item selection, and/or get currently selected items
  6772. * @param {Array} [ids] An array with zero or more ids of the items to be selected.
  6773. * @return {Array} ids The ids of the selected items
  6774. */
  6775. Timeline.prototype.select = function select(ids) {
  6776. return this.content ? this.content.select(ids) : [];
  6777. };
  6778. (function(exports) {
  6779. /**
  6780. * Parse a text source containing data in DOT language into a JSON object.
  6781. * The object contains two lists: one with nodes and one with edges.
  6782. *
  6783. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  6784. *
  6785. * @param {String} data Text containing a graph in DOT-notation
  6786. * @return {Object} graph An object containing two parameters:
  6787. * {Object[]} nodes
  6788. * {Object[]} edges
  6789. */
  6790. function parseDOT (data) {
  6791. dot = data;
  6792. return parseGraph();
  6793. }
  6794. // token types enumeration
  6795. var TOKENTYPE = {
  6796. NULL : 0,
  6797. DELIMITER : 1,
  6798. IDENTIFIER: 2,
  6799. UNKNOWN : 3
  6800. };
  6801. // map with all delimiters
  6802. var DELIMITERS = {
  6803. '{': true,
  6804. '}': true,
  6805. '[': true,
  6806. ']': true,
  6807. ';': true,
  6808. '=': true,
  6809. ',': true,
  6810. '->': true,
  6811. '--': true
  6812. };
  6813. var dot = ''; // current dot file
  6814. var index = 0; // current index in dot file
  6815. var c = ''; // current token character in expr
  6816. var token = ''; // current token
  6817. var tokenType = TOKENTYPE.NULL; // type of the token
  6818. /**
  6819. * Get the first character from the dot file.
  6820. * The character is stored into the char c. If the end of the dot file is
  6821. * reached, the function puts an empty string in c.
  6822. */
  6823. function first() {
  6824. index = 0;
  6825. c = dot.charAt(0);
  6826. }
  6827. /**
  6828. * Get the next character from the dot file.
  6829. * The character is stored into the char c. If the end of the dot file is
  6830. * reached, the function puts an empty string in c.
  6831. */
  6832. function next() {
  6833. index++;
  6834. c = dot.charAt(index);
  6835. }
  6836. /**
  6837. * Preview the next character from the dot file.
  6838. * @return {String} cNext
  6839. */
  6840. function nextPreview() {
  6841. return dot.charAt(index + 1);
  6842. }
  6843. /**
  6844. * Test whether given character is alphabetic or numeric
  6845. * @param {String} c
  6846. * @return {Boolean} isAlphaNumeric
  6847. */
  6848. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  6849. function isAlphaNumeric(c) {
  6850. return regexAlphaNumeric.test(c);
  6851. }
  6852. /**
  6853. * Merge all properties of object b into object b
  6854. * @param {Object} a
  6855. * @param {Object} b
  6856. * @return {Object} a
  6857. */
  6858. function merge (a, b) {
  6859. if (!a) {
  6860. a = {};
  6861. }
  6862. if (b) {
  6863. for (var name in b) {
  6864. if (b.hasOwnProperty(name)) {
  6865. a[name] = b[name];
  6866. }
  6867. }
  6868. }
  6869. return a;
  6870. }
  6871. /**
  6872. * Set a value in an object, where the provided parameter name can be a
  6873. * path with nested parameters. For example:
  6874. *
  6875. * var obj = {a: 2};
  6876. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  6877. *
  6878. * @param {Object} obj
  6879. * @param {String} path A parameter name or dot-separated parameter path,
  6880. * like "color.highlight.border".
  6881. * @param {*} value
  6882. */
  6883. function setValue(obj, path, value) {
  6884. var keys = path.split('.');
  6885. var o = obj;
  6886. while (keys.length) {
  6887. var key = keys.shift();
  6888. if (keys.length) {
  6889. // this isn't the end point
  6890. if (!o[key]) {
  6891. o[key] = {};
  6892. }
  6893. o = o[key];
  6894. }
  6895. else {
  6896. // this is the end point
  6897. o[key] = value;
  6898. }
  6899. }
  6900. }
  6901. /**
  6902. * Add a node to a graph object. If there is already a node with
  6903. * the same id, their attributes will be merged.
  6904. * @param {Object} graph
  6905. * @param {Object} node
  6906. */
  6907. function addNode(graph, node) {
  6908. var i, len;
  6909. var current = null;
  6910. // find root graph (in case of subgraph)
  6911. var graphs = [graph]; // list with all graphs from current graph to root graph
  6912. var root = graph;
  6913. while (root.parent) {
  6914. graphs.push(root.parent);
  6915. root = root.parent;
  6916. }
  6917. // find existing node (at root level) by its id
  6918. if (root.nodes) {
  6919. for (i = 0, len = root.nodes.length; i < len; i++) {
  6920. if (node.id === root.nodes[i].id) {
  6921. current = root.nodes[i];
  6922. break;
  6923. }
  6924. }
  6925. }
  6926. if (!current) {
  6927. // this is a new node
  6928. current = {
  6929. id: node.id
  6930. };
  6931. if (graph.node) {
  6932. // clone default attributes
  6933. current.attr = merge(current.attr, graph.node);
  6934. }
  6935. }
  6936. // add node to this (sub)graph and all its parent graphs
  6937. for (i = graphs.length - 1; i >= 0; i--) {
  6938. var g = graphs[i];
  6939. if (!g.nodes) {
  6940. g.nodes = [];
  6941. }
  6942. if (g.nodes.indexOf(current) == -1) {
  6943. g.nodes.push(current);
  6944. }
  6945. }
  6946. // merge attributes
  6947. if (node.attr) {
  6948. current.attr = merge(current.attr, node.attr);
  6949. }
  6950. }
  6951. /**
  6952. * Add an edge to a graph object
  6953. * @param {Object} graph
  6954. * @param {Object} edge
  6955. */
  6956. function addEdge(graph, edge) {
  6957. if (!graph.edges) {
  6958. graph.edges = [];
  6959. }
  6960. graph.edges.push(edge);
  6961. if (graph.edge) {
  6962. var attr = merge({}, graph.edge); // clone default attributes
  6963. edge.attr = merge(attr, edge.attr); // merge attributes
  6964. }
  6965. }
  6966. /**
  6967. * Create an edge to a graph object
  6968. * @param {Object} graph
  6969. * @param {String | Number | Object} from
  6970. * @param {String | Number | Object} to
  6971. * @param {String} type
  6972. * @param {Object | null} attr
  6973. * @return {Object} edge
  6974. */
  6975. function createEdge(graph, from, to, type, attr) {
  6976. var edge = {
  6977. from: from,
  6978. to: to,
  6979. type: type
  6980. };
  6981. if (graph.edge) {
  6982. edge.attr = merge({}, graph.edge); // clone default attributes
  6983. }
  6984. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  6985. return edge;
  6986. }
  6987. /**
  6988. * Get next token in the current dot file.
  6989. * The token and token type are available as token and tokenType
  6990. */
  6991. function getToken() {
  6992. tokenType = TOKENTYPE.NULL;
  6993. token = '';
  6994. // skip over whitespaces
  6995. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  6996. next();
  6997. }
  6998. do {
  6999. var isComment = false;
  7000. // skip comment
  7001. if (c == '#') {
  7002. // find the previous non-space character
  7003. var i = index - 1;
  7004. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  7005. i--;
  7006. }
  7007. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  7008. // the # is at the start of a line, this is indeed a line comment
  7009. while (c != '' && c != '\n') {
  7010. next();
  7011. }
  7012. isComment = true;
  7013. }
  7014. }
  7015. if (c == '/' && nextPreview() == '/') {
  7016. // skip line comment
  7017. while (c != '' && c != '\n') {
  7018. next();
  7019. }
  7020. isComment = true;
  7021. }
  7022. if (c == '/' && nextPreview() == '*') {
  7023. // skip block comment
  7024. while (c != '') {
  7025. if (c == '*' && nextPreview() == '/') {
  7026. // end of block comment found. skip these last two characters
  7027. next();
  7028. next();
  7029. break;
  7030. }
  7031. else {
  7032. next();
  7033. }
  7034. }
  7035. isComment = true;
  7036. }
  7037. // skip over whitespaces
  7038. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  7039. next();
  7040. }
  7041. }
  7042. while (isComment);
  7043. // check for end of dot file
  7044. if (c == '') {
  7045. // token is still empty
  7046. tokenType = TOKENTYPE.DELIMITER;
  7047. return;
  7048. }
  7049. // check for delimiters consisting of 2 characters
  7050. var c2 = c + nextPreview();
  7051. if (DELIMITERS[c2]) {
  7052. tokenType = TOKENTYPE.DELIMITER;
  7053. token = c2;
  7054. next();
  7055. next();
  7056. return;
  7057. }
  7058. // check for delimiters consisting of 1 character
  7059. if (DELIMITERS[c]) {
  7060. tokenType = TOKENTYPE.DELIMITER;
  7061. token = c;
  7062. next();
  7063. return;
  7064. }
  7065. // check for an identifier (number or string)
  7066. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  7067. if (isAlphaNumeric(c) || c == '-') {
  7068. token += c;
  7069. next();
  7070. while (isAlphaNumeric(c)) {
  7071. token += c;
  7072. next();
  7073. }
  7074. if (token == 'false') {
  7075. token = false; // convert to boolean
  7076. }
  7077. else if (token == 'true') {
  7078. token = true; // convert to boolean
  7079. }
  7080. else if (!isNaN(Number(token))) {
  7081. token = Number(token); // convert to number
  7082. }
  7083. tokenType = TOKENTYPE.IDENTIFIER;
  7084. return;
  7085. }
  7086. // check for a string enclosed by double quotes
  7087. if (c == '"') {
  7088. next();
  7089. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  7090. token += c;
  7091. if (c == '"') { // skip the escape character
  7092. next();
  7093. }
  7094. next();
  7095. }
  7096. if (c != '"') {
  7097. throw newSyntaxError('End of string " expected');
  7098. }
  7099. next();
  7100. tokenType = TOKENTYPE.IDENTIFIER;
  7101. return;
  7102. }
  7103. // something unknown is found, wrong characters, a syntax error
  7104. tokenType = TOKENTYPE.UNKNOWN;
  7105. while (c != '') {
  7106. token += c;
  7107. next();
  7108. }
  7109. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  7110. }
  7111. /**
  7112. * Parse a graph.
  7113. * @returns {Object} graph
  7114. */
  7115. function parseGraph() {
  7116. var graph = {};
  7117. first();
  7118. getToken();
  7119. // optional strict keyword
  7120. if (token == 'strict') {
  7121. graph.strict = true;
  7122. getToken();
  7123. }
  7124. // graph or digraph keyword
  7125. if (token == 'graph' || token == 'digraph') {
  7126. graph.type = token;
  7127. getToken();
  7128. }
  7129. // optional graph id
  7130. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7131. graph.id = token;
  7132. getToken();
  7133. }
  7134. // open angle bracket
  7135. if (token != '{') {
  7136. throw newSyntaxError('Angle bracket { expected');
  7137. }
  7138. getToken();
  7139. // statements
  7140. parseStatements(graph);
  7141. // close angle bracket
  7142. if (token != '}') {
  7143. throw newSyntaxError('Angle bracket } expected');
  7144. }
  7145. getToken();
  7146. // end of file
  7147. if (token !== '') {
  7148. throw newSyntaxError('End of file expected');
  7149. }
  7150. getToken();
  7151. // remove temporary default properties
  7152. delete graph.node;
  7153. delete graph.edge;
  7154. delete graph.graph;
  7155. return graph;
  7156. }
  7157. /**
  7158. * Parse a list with statements.
  7159. * @param {Object} graph
  7160. */
  7161. function parseStatements (graph) {
  7162. while (token !== '' && token != '}') {
  7163. parseStatement(graph);
  7164. if (token == ';') {
  7165. getToken();
  7166. }
  7167. }
  7168. }
  7169. /**
  7170. * Parse a single statement. Can be a an attribute statement, node
  7171. * statement, a series of node statements and edge statements, or a
  7172. * parameter.
  7173. * @param {Object} graph
  7174. */
  7175. function parseStatement(graph) {
  7176. // parse subgraph
  7177. var subgraph = parseSubgraph(graph);
  7178. if (subgraph) {
  7179. // edge statements
  7180. parseEdge(graph, subgraph);
  7181. return;
  7182. }
  7183. // parse an attribute statement
  7184. var attr = parseAttributeStatement(graph);
  7185. if (attr) {
  7186. return;
  7187. }
  7188. // parse node
  7189. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7190. throw newSyntaxError('Identifier expected');
  7191. }
  7192. var id = token; // id can be a string or a number
  7193. getToken();
  7194. if (token == '=') {
  7195. // id statement
  7196. getToken();
  7197. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7198. throw newSyntaxError('Identifier expected');
  7199. }
  7200. graph[id] = token;
  7201. getToken();
  7202. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  7203. }
  7204. else {
  7205. parseNodeStatement(graph, id);
  7206. }
  7207. }
  7208. /**
  7209. * Parse a subgraph
  7210. * @param {Object} graph parent graph object
  7211. * @return {Object | null} subgraph
  7212. */
  7213. function parseSubgraph (graph) {
  7214. var subgraph = null;
  7215. // optional subgraph keyword
  7216. if (token == 'subgraph') {
  7217. subgraph = {};
  7218. subgraph.type = 'subgraph';
  7219. getToken();
  7220. // optional graph id
  7221. if (tokenType == TOKENTYPE.IDENTIFIER) {
  7222. subgraph.id = token;
  7223. getToken();
  7224. }
  7225. }
  7226. // open angle bracket
  7227. if (token == '{') {
  7228. getToken();
  7229. if (!subgraph) {
  7230. subgraph = {};
  7231. }
  7232. subgraph.parent = graph;
  7233. subgraph.node = graph.node;
  7234. subgraph.edge = graph.edge;
  7235. subgraph.graph = graph.graph;
  7236. // statements
  7237. parseStatements(subgraph);
  7238. // close angle bracket
  7239. if (token != '}') {
  7240. throw newSyntaxError('Angle bracket } expected');
  7241. }
  7242. getToken();
  7243. // remove temporary default properties
  7244. delete subgraph.node;
  7245. delete subgraph.edge;
  7246. delete subgraph.graph;
  7247. delete subgraph.parent;
  7248. // register at the parent graph
  7249. if (!graph.subgraphs) {
  7250. graph.subgraphs = [];
  7251. }
  7252. graph.subgraphs.push(subgraph);
  7253. }
  7254. return subgraph;
  7255. }
  7256. /**
  7257. * parse an attribute statement like "node [shape=circle fontSize=16]".
  7258. * Available keywords are 'node', 'edge', 'graph'.
  7259. * The previous list with default attributes will be replaced
  7260. * @param {Object} graph
  7261. * @returns {String | null} keyword Returns the name of the parsed attribute
  7262. * (node, edge, graph), or null if nothing
  7263. * is parsed.
  7264. */
  7265. function parseAttributeStatement (graph) {
  7266. // attribute statements
  7267. if (token == 'node') {
  7268. getToken();
  7269. // node attributes
  7270. graph.node = parseAttributeList();
  7271. return 'node';
  7272. }
  7273. else if (token == 'edge') {
  7274. getToken();
  7275. // edge attributes
  7276. graph.edge = parseAttributeList();
  7277. return 'edge';
  7278. }
  7279. else if (token == 'graph') {
  7280. getToken();
  7281. // graph attributes
  7282. graph.graph = parseAttributeList();
  7283. return 'graph';
  7284. }
  7285. return null;
  7286. }
  7287. /**
  7288. * parse a node statement
  7289. * @param {Object} graph
  7290. * @param {String | Number} id
  7291. */
  7292. function parseNodeStatement(graph, id) {
  7293. // node statement
  7294. var node = {
  7295. id: id
  7296. };
  7297. var attr = parseAttributeList();
  7298. if (attr) {
  7299. node.attr = attr;
  7300. }
  7301. addNode(graph, node);
  7302. // edge statements
  7303. parseEdge(graph, id);
  7304. }
  7305. /**
  7306. * Parse an edge or a series of edges
  7307. * @param {Object} graph
  7308. * @param {String | Number} from Id of the from node
  7309. */
  7310. function parseEdge(graph, from) {
  7311. while (token == '->' || token == '--') {
  7312. var to;
  7313. var type = token;
  7314. getToken();
  7315. var subgraph = parseSubgraph(graph);
  7316. if (subgraph) {
  7317. to = subgraph;
  7318. }
  7319. else {
  7320. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7321. throw newSyntaxError('Identifier or subgraph expected');
  7322. }
  7323. to = token;
  7324. addNode(graph, {
  7325. id: to
  7326. });
  7327. getToken();
  7328. }
  7329. // parse edge attributes
  7330. var attr = parseAttributeList();
  7331. // create edge
  7332. var edge = createEdge(graph, from, to, type, attr);
  7333. addEdge(graph, edge);
  7334. from = to;
  7335. }
  7336. }
  7337. /**
  7338. * Parse a set with attributes,
  7339. * for example [label="1.000", shape=solid]
  7340. * @return {Object | null} attr
  7341. */
  7342. function parseAttributeList() {
  7343. var attr = null;
  7344. while (token == '[') {
  7345. getToken();
  7346. attr = {};
  7347. while (token !== '' && token != ']') {
  7348. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7349. throw newSyntaxError('Attribute name expected');
  7350. }
  7351. var name = token;
  7352. getToken();
  7353. if (token != '=') {
  7354. throw newSyntaxError('Equal sign = expected');
  7355. }
  7356. getToken();
  7357. if (tokenType != TOKENTYPE.IDENTIFIER) {
  7358. throw newSyntaxError('Attribute value expected');
  7359. }
  7360. var value = token;
  7361. setValue(attr, name, value); // name can be a path
  7362. getToken();
  7363. if (token ==',') {
  7364. getToken();
  7365. }
  7366. }
  7367. if (token != ']') {
  7368. throw newSyntaxError('Bracket ] expected');
  7369. }
  7370. getToken();
  7371. }
  7372. return attr;
  7373. }
  7374. /**
  7375. * Create a syntax error with extra information on current token and index.
  7376. * @param {String} message
  7377. * @returns {SyntaxError} err
  7378. */
  7379. function newSyntaxError(message) {
  7380. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  7381. }
  7382. /**
  7383. * Chop off text after a maximum length
  7384. * @param {String} text
  7385. * @param {Number} maxLength
  7386. * @returns {String}
  7387. */
  7388. function chop (text, maxLength) {
  7389. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  7390. }
  7391. /**
  7392. * Execute a function fn for each pair of elements in two arrays
  7393. * @param {Array | *} array1
  7394. * @param {Array | *} array2
  7395. * @param {function} fn
  7396. */
  7397. function forEach2(array1, array2, fn) {
  7398. if (array1 instanceof Array) {
  7399. array1.forEach(function (elem1) {
  7400. if (array2 instanceof Array) {
  7401. array2.forEach(function (elem2) {
  7402. fn(elem1, elem2);
  7403. });
  7404. }
  7405. else {
  7406. fn(elem1, array2);
  7407. }
  7408. });
  7409. }
  7410. else {
  7411. if (array2 instanceof Array) {
  7412. array2.forEach(function (elem2) {
  7413. fn(array1, elem2);
  7414. });
  7415. }
  7416. else {
  7417. fn(array1, array2);
  7418. }
  7419. }
  7420. }
  7421. /**
  7422. * Convert a string containing a graph in DOT language into a map containing
  7423. * with nodes and edges in the format of graph.
  7424. * @param {String} data Text containing a graph in DOT-notation
  7425. * @return {Object} graphData
  7426. */
  7427. function DOTToGraph (data) {
  7428. // parse the DOT file
  7429. var dotData = parseDOT(data);
  7430. var graphData = {
  7431. nodes: [],
  7432. edges: [],
  7433. options: {}
  7434. };
  7435. // copy the nodes
  7436. if (dotData.nodes) {
  7437. dotData.nodes.forEach(function (dotNode) {
  7438. var graphNode = {
  7439. id: dotNode.id,
  7440. label: String(dotNode.label || dotNode.id)
  7441. };
  7442. merge(graphNode, dotNode.attr);
  7443. if (graphNode.image) {
  7444. graphNode.shape = 'image';
  7445. }
  7446. graphData.nodes.push(graphNode);
  7447. });
  7448. }
  7449. // copy the edges
  7450. if (dotData.edges) {
  7451. /**
  7452. * Convert an edge in DOT format to an edge with VisGraph format
  7453. * @param {Object} dotEdge
  7454. * @returns {Object} graphEdge
  7455. */
  7456. function convertEdge(dotEdge) {
  7457. var graphEdge = {
  7458. from: dotEdge.from,
  7459. to: dotEdge.to
  7460. };
  7461. merge(graphEdge, dotEdge.attr);
  7462. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  7463. return graphEdge;
  7464. }
  7465. dotData.edges.forEach(function (dotEdge) {
  7466. var from, to;
  7467. if (dotEdge.from instanceof Object) {
  7468. from = dotEdge.from.nodes;
  7469. }
  7470. else {
  7471. from = {
  7472. id: dotEdge.from
  7473. }
  7474. }
  7475. if (dotEdge.to instanceof Object) {
  7476. to = dotEdge.to.nodes;
  7477. }
  7478. else {
  7479. to = {
  7480. id: dotEdge.to
  7481. }
  7482. }
  7483. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  7484. dotEdge.from.edges.forEach(function (subEdge) {
  7485. var graphEdge = convertEdge(subEdge);
  7486. graphData.edges.push(graphEdge);
  7487. });
  7488. }
  7489. forEach2(from, to, function (from, to) {
  7490. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  7491. var graphEdge = convertEdge(subEdge);
  7492. graphData.edges.push(graphEdge);
  7493. });
  7494. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  7495. dotEdge.to.edges.forEach(function (subEdge) {
  7496. var graphEdge = convertEdge(subEdge);
  7497. graphData.edges.push(graphEdge);
  7498. });
  7499. }
  7500. });
  7501. }
  7502. // copy the options
  7503. if (dotData.attr) {
  7504. graphData.options = dotData.attr;
  7505. }
  7506. return graphData;
  7507. }
  7508. // exports
  7509. exports.parseDOT = parseDOT;
  7510. exports.DOTToGraph = DOTToGraph;
  7511. })(typeof util !== 'undefined' ? util : exports);
  7512. /**
  7513. * Canvas shapes used by the Graph
  7514. */
  7515. if (typeof CanvasRenderingContext2D !== 'undefined') {
  7516. /**
  7517. * Draw a circle shape
  7518. */
  7519. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  7520. this.beginPath();
  7521. this.arc(x, y, r, 0, 2*Math.PI, false);
  7522. };
  7523. /**
  7524. * Draw a square shape
  7525. * @param {Number} x horizontal center
  7526. * @param {Number} y vertical center
  7527. * @param {Number} r size, width and height of the square
  7528. */
  7529. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  7530. this.beginPath();
  7531. this.rect(x - r, y - r, r * 2, r * 2);
  7532. };
  7533. /**
  7534. * Draw a triangle shape
  7535. * @param {Number} x horizontal center
  7536. * @param {Number} y vertical center
  7537. * @param {Number} r radius, half the length of the sides of the triangle
  7538. */
  7539. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  7540. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7541. this.beginPath();
  7542. var s = r * 2;
  7543. var s2 = s / 2;
  7544. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7545. var h = Math.sqrt(s * s - s2 * s2); // height
  7546. this.moveTo(x, y - (h - ir));
  7547. this.lineTo(x + s2, y + ir);
  7548. this.lineTo(x - s2, y + ir);
  7549. this.lineTo(x, y - (h - ir));
  7550. this.closePath();
  7551. };
  7552. /**
  7553. * Draw a triangle shape in downward orientation
  7554. * @param {Number} x horizontal center
  7555. * @param {Number} y vertical center
  7556. * @param {Number} r radius
  7557. */
  7558. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  7559. // http://en.wikipedia.org/wiki/Equilateral_triangle
  7560. this.beginPath();
  7561. var s = r * 2;
  7562. var s2 = s / 2;
  7563. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  7564. var h = Math.sqrt(s * s - s2 * s2); // height
  7565. this.moveTo(x, y + (h - ir));
  7566. this.lineTo(x + s2, y - ir);
  7567. this.lineTo(x - s2, y - ir);
  7568. this.lineTo(x, y + (h - ir));
  7569. this.closePath();
  7570. };
  7571. /**
  7572. * Draw a star shape, a star with 5 points
  7573. * @param {Number} x horizontal center
  7574. * @param {Number} y vertical center
  7575. * @param {Number} r radius, half the length of the sides of the triangle
  7576. */
  7577. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  7578. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  7579. this.beginPath();
  7580. for (var n = 0; n < 10; n++) {
  7581. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  7582. this.lineTo(
  7583. x + radius * Math.sin(n * 2 * Math.PI / 10),
  7584. y - radius * Math.cos(n * 2 * Math.PI / 10)
  7585. );
  7586. }
  7587. this.closePath();
  7588. };
  7589. /**
  7590. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  7591. */
  7592. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  7593. var r2d = Math.PI/180;
  7594. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  7595. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  7596. this.beginPath();
  7597. this.moveTo(x+r,y);
  7598. this.lineTo(x+w-r,y);
  7599. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  7600. this.lineTo(x+w,y+h-r);
  7601. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  7602. this.lineTo(x+r,y+h);
  7603. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  7604. this.lineTo(x,y+r);
  7605. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  7606. };
  7607. /**
  7608. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7609. */
  7610. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  7611. var kappa = .5522848,
  7612. ox = (w / 2) * kappa, // control point offset horizontal
  7613. oy = (h / 2) * kappa, // control point offset vertical
  7614. xe = x + w, // x-end
  7615. ye = y + h, // y-end
  7616. xm = x + w / 2, // x-middle
  7617. ym = y + h / 2; // y-middle
  7618. this.beginPath();
  7619. this.moveTo(x, ym);
  7620. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7621. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7622. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7623. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7624. };
  7625. /**
  7626. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  7627. */
  7628. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  7629. var f = 1/3;
  7630. var wEllipse = w;
  7631. var hEllipse = h * f;
  7632. var kappa = .5522848,
  7633. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  7634. oy = (hEllipse / 2) * kappa, // control point offset vertical
  7635. xe = x + wEllipse, // x-end
  7636. ye = y + hEllipse, // y-end
  7637. xm = x + wEllipse / 2, // x-middle
  7638. ym = y + hEllipse / 2, // y-middle
  7639. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  7640. yeb = y + h; // y-end, bottom ellipse
  7641. this.beginPath();
  7642. this.moveTo(xe, ym);
  7643. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  7644. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  7645. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  7646. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  7647. this.lineTo(xe, ymb);
  7648. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  7649. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  7650. this.lineTo(x, ym);
  7651. };
  7652. /**
  7653. * Draw an arrow point (no line)
  7654. */
  7655. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  7656. // tail
  7657. var xt = x - length * Math.cos(angle);
  7658. var yt = y - length * Math.sin(angle);
  7659. // inner tail
  7660. // TODO: allow to customize different shapes
  7661. var xi = x - length * 0.9 * Math.cos(angle);
  7662. var yi = y - length * 0.9 * Math.sin(angle);
  7663. // left
  7664. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  7665. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  7666. // right
  7667. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  7668. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  7669. this.beginPath();
  7670. this.moveTo(x, y);
  7671. this.lineTo(xl, yl);
  7672. this.lineTo(xi, yi);
  7673. this.lineTo(xr, yr);
  7674. this.closePath();
  7675. };
  7676. /**
  7677. * Sets up the dashedLine functionality for drawing
  7678. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  7679. * @author David Jordan
  7680. * @date 2012-08-08
  7681. */
  7682. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  7683. if (!dashArray) dashArray=[10,5];
  7684. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  7685. var dashCount = dashArray.length;
  7686. this.moveTo(x, y);
  7687. var dx = (x2-x), dy = (y2-y);
  7688. var slope = dy/dx;
  7689. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  7690. var dashIndex=0, draw=true;
  7691. while (distRemaining>=0.1){
  7692. var dashLength = dashArray[dashIndex++%dashCount];
  7693. if (dashLength > distRemaining) dashLength = distRemaining;
  7694. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  7695. if (dx<0) xStep = -xStep;
  7696. x += xStep;
  7697. y += slope*xStep;
  7698. this[draw ? 'lineTo' : 'moveTo'](x,y);
  7699. distRemaining -= dashLength;
  7700. draw = !draw;
  7701. }
  7702. };
  7703. // TODO: add diamond shape
  7704. }
  7705. /**
  7706. * @class Node
  7707. * A node. A node can be connected to other nodes via one or multiple edges.
  7708. * @param {object} properties An object containing properties for the node. All
  7709. * properties are optional, except for the id.
  7710. * {number} id Id of the node. Required
  7711. * {string} label Text label for the node
  7712. * {number} x Horizontal position of the node
  7713. * {number} y Vertical position of the node
  7714. * {string} shape Node shape, available:
  7715. * "database", "circle", "ellipse",
  7716. * "box", "image", "text", "dot",
  7717. * "star", "triangle", "triangleDown",
  7718. * "square"
  7719. * {string} image An image url
  7720. * {string} title An title text, can be HTML
  7721. * {anytype} group A group name or number
  7722. * @param {Graph.Images} imagelist A list with images. Only needed
  7723. * when the node has an image
  7724. * @param {Graph.Groups} grouplist A list with groups. Needed for
  7725. * retrieving group properties
  7726. * @param {Object} constants An object with default values for
  7727. * example for the color
  7728. */
  7729. function Node(properties, imagelist, grouplist, constants) {
  7730. this.selected = false;
  7731. this.edges = []; // all edges connected to this node
  7732. this.dynamicEdges = [];
  7733. this.reroutedEdges = {};
  7734. this.group = constants.nodes.group;
  7735. this.fontSize = constants.nodes.fontSize;
  7736. this.fontFace = constants.nodes.fontFace;
  7737. this.fontColor = constants.nodes.fontColor;
  7738. this.color = constants.nodes.color;
  7739. // set defaults for the properties
  7740. this.id = undefined;
  7741. this.shape = constants.nodes.shape;
  7742. this.image = constants.nodes.image;
  7743. this.x = 0;
  7744. this.y = 0;
  7745. this.xFixed = false;
  7746. this.yFixed = false;
  7747. this.radius = constants.nodes.radius;
  7748. this.baseRadiusValue = constants.nodes.radius;
  7749. this.radiusFixed = false;
  7750. this.radiusMin = constants.nodes.radiusMin;
  7751. this.radiusMax = constants.nodes.radiusMax;
  7752. this.imagelist = imagelist;
  7753. this.grouplist = grouplist;
  7754. this.setProperties(properties, constants);
  7755. // creating the variables for clustering
  7756. this.resetCluster();
  7757. this.dynamicEdgesLength = 0;
  7758. this.clusterSession = 0;
  7759. this.clusterSizeWidthFactor = constants.clustering.clusterSizeWidthFactor;
  7760. this.clusterSizeHeightFactor = constants.clustering.clusterSizeHeightFactor;
  7761. this.clusterSizeRadiusFactor = constants.clustering.clusterSizeRadiusFactor;
  7762. // mass, force, velocity
  7763. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  7764. this.fx = 0.0; // external force x
  7765. this.fy = 0.0; // external force y
  7766. this.vx = 0.0; // velocity x
  7767. this.vy = 0.0; // velocity y
  7768. this.minForce = constants.minForce;
  7769. this.damping = 0.9; // damping factor
  7770. this.graphScaleInv = 1;
  7771. this.canvasTopLeft = {"x": -300, "y": -300};
  7772. this.canvasBottomRight = {"x": 300, "y": 300};
  7773. }
  7774. /**
  7775. * (re)setting the clustering variables and objects
  7776. */
  7777. Node.prototype.resetCluster = function() {
  7778. // clustering variables
  7779. this.formationScale = undefined; // this is used to determine when to open the cluster
  7780. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  7781. this.containedNodes = {};
  7782. this.containedEdges = {};
  7783. this.clusterSessions = [];
  7784. };
  7785. /**
  7786. * Attach a edge to the node
  7787. * @param {Edge} edge
  7788. */
  7789. Node.prototype.attachEdge = function(edge) {
  7790. if (this.edges.indexOf(edge) == -1) {
  7791. this.edges.push(edge);
  7792. }
  7793. if (this.dynamicEdges.indexOf(edge) == -1) {
  7794. this.dynamicEdges.push(edge);
  7795. }
  7796. this.dynamicEdgesLength = this.dynamicEdges.length;
  7797. this._updateMass();
  7798. };
  7799. /**
  7800. * Detach a edge from the node
  7801. * @param {Edge} edge
  7802. */
  7803. Node.prototype.detachEdge = function(edge) {
  7804. var index = this.edges.indexOf(edge);
  7805. if (index != -1) {
  7806. this.edges.splice(index, 1);
  7807. this.dynamicEdges.splice(index, 1);
  7808. }
  7809. this.dynamicEdgesLength = this.dynamicEdges.length;
  7810. this._updateMass();
  7811. };
  7812. /**
  7813. * Update the nodes mass, which is determined by the number of edges connecting
  7814. * to it (more edges -> heavier node).
  7815. * @private
  7816. */
  7817. Node.prototype._updateMass = function() {
  7818. this.mass = 50 + 20 * this.edges.length; // kg
  7819. };
  7820. /**
  7821. * Set or overwrite properties for the node
  7822. * @param {Object} properties an object with properties
  7823. * @param {Object} constants and object with default, global properties
  7824. */
  7825. Node.prototype.setProperties = function(properties, constants) {
  7826. if (!properties) {
  7827. return;
  7828. }
  7829. this.originalLabel = undefined;
  7830. // basic properties
  7831. if (properties.id !== undefined) {this.id = properties.id;}
  7832. if (properties.label !== undefined) {this.label = properties.label; this.originalLabel = properties.label;}
  7833. if (properties.title !== undefined) {this.title = properties.title;}
  7834. if (properties.group !== undefined) {this.group = properties.group;}
  7835. if (properties.x !== undefined) {this.x = properties.x;}
  7836. if (properties.y !== undefined) {this.y = properties.y;}
  7837. if (properties.value !== undefined) {this.value = properties.value;}
  7838. if (this.id === undefined) {
  7839. throw "Node must have an id";
  7840. }
  7841. // copy group properties
  7842. if (this.group) {
  7843. var groupObj = this.grouplist.get(this.group);
  7844. for (var prop in groupObj) {
  7845. if (groupObj.hasOwnProperty(prop)) {
  7846. this[prop] = groupObj[prop];
  7847. }
  7848. }
  7849. }
  7850. // individual shape properties
  7851. if (properties.shape !== undefined) {this.shape = properties.shape;}
  7852. if (properties.image !== undefined) {this.image = properties.image;}
  7853. if (properties.radius !== undefined) {this.radius = properties.radius;}
  7854. if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);}
  7855. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  7856. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  7857. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  7858. if (this.image !== undefined) {
  7859. if (this.imagelist) {
  7860. this.imageObj = this.imagelist.load(this.image);
  7861. }
  7862. else {
  7863. throw "No imagelist provided";
  7864. }
  7865. }
  7866. this.xFixed = this.xFixed || (properties.x !== undefined);
  7867. this.yFixed = this.yFixed || (properties.y !== undefined);
  7868. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  7869. if (this.shape == 'image') {
  7870. this.radiusMin = constants.nodes.widthMin;
  7871. this.radiusMax = constants.nodes.widthMax;
  7872. }
  7873. // choose draw method depending on the shape
  7874. switch (this.shape) {
  7875. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  7876. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  7877. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  7878. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7879. // TODO: add diamond shape
  7880. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  7881. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  7882. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  7883. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  7884. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  7885. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  7886. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  7887. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  7888. }
  7889. // reset the size of the node, this can be changed
  7890. this._reset();
  7891. };
  7892. /**
  7893. * Parse a color property into an object with border, background, and
  7894. * hightlight colors
  7895. * @param {Object | String} color
  7896. * @return {Object} colorObject
  7897. */
  7898. Node.parseColor = function(color) {
  7899. var c;
  7900. if (util.isString(color)) {
  7901. c = {
  7902. border: color,
  7903. background: color,
  7904. highlight: {
  7905. border: color,
  7906. background: color
  7907. }
  7908. };
  7909. // TODO: automatically generate a nice highlight color
  7910. }
  7911. else {
  7912. c = {};
  7913. c.background = color.background || 'white';
  7914. c.border = color.border || c.background;
  7915. if (util.isString(color.highlight)) {
  7916. c.highlight = {
  7917. border: color.highlight,
  7918. background: color.highlight
  7919. }
  7920. }
  7921. else {
  7922. c.highlight = {};
  7923. c.highlight.background = color.highlight && color.highlight.background || c.background;
  7924. c.highlight.border = color.highlight && color.highlight.border || c.border;
  7925. }
  7926. }
  7927. return c;
  7928. };
  7929. /**
  7930. * select this node
  7931. */
  7932. Node.prototype.select = function() {
  7933. this.selected = true;
  7934. this._reset();
  7935. };
  7936. /**
  7937. * unselect this node
  7938. */
  7939. Node.prototype.unselect = function() {
  7940. this.selected = false;
  7941. this._reset();
  7942. };
  7943. /**
  7944. * Reset the calculated size of the node, forces it to recalculate its size
  7945. */
  7946. Node.prototype.clearSizeCache = function() {
  7947. this._reset();
  7948. };
  7949. /**
  7950. * Reset the calculated size of the node, forces it to recalculate its size
  7951. * @private
  7952. */
  7953. Node.prototype._reset = function() {
  7954. this.width = undefined;
  7955. this.height = undefined;
  7956. };
  7957. /**
  7958. * get the title of this node.
  7959. * @return {string} title The title of the node, or undefined when no title
  7960. * has been set.
  7961. */
  7962. Node.prototype.getTitle = function() {
  7963. return this.title;
  7964. };
  7965. /**
  7966. * Calculate the distance to the border of the Node
  7967. * @param {CanvasRenderingContext2D} ctx
  7968. * @param {Number} angle Angle in radians
  7969. * @returns {number} distance Distance to the border in pixels
  7970. */
  7971. Node.prototype.distanceToBorder = function (ctx, angle) {
  7972. var borderWidth = 1;
  7973. if (!this.width) {
  7974. this.resize(ctx);
  7975. }
  7976. //noinspection FallthroughInSwitchStatementJS
  7977. switch (this.shape) {
  7978. case 'circle':
  7979. case 'dot':
  7980. return this.radius + borderWidth;
  7981. case 'ellipse':
  7982. var a = this.width / 2;
  7983. var b = this.height / 2;
  7984. var w = (Math.sin(angle) * a);
  7985. var h = (Math.cos(angle) * b);
  7986. return a * b / Math.sqrt(w * w + h * h);
  7987. // TODO: implement distanceToBorder for database
  7988. // TODO: implement distanceToBorder for triangle
  7989. // TODO: implement distanceToBorder for triangleDown
  7990. case 'box':
  7991. case 'image':
  7992. case 'text':
  7993. default:
  7994. if (this.width) {
  7995. return Math.min(
  7996. Math.abs(this.width / 2 / Math.cos(angle)),
  7997. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  7998. // TODO: reckon with border radius too in case of box
  7999. }
  8000. else {
  8001. return 0;
  8002. }
  8003. }
  8004. // TODO: implement calculation of distance to border for all shapes
  8005. };
  8006. /**
  8007. * Set forces acting on the node
  8008. * @param {number} fx Force in horizontal direction
  8009. * @param {number} fy Force in vertical direction
  8010. */
  8011. Node.prototype._setForce = function(fx, fy) {
  8012. this.fx = fx;
  8013. this.fy = fy;
  8014. };
  8015. /**
  8016. * Add forces acting on the node
  8017. * @param {number} fx Force in horizontal direction
  8018. * @param {number} fy Force in vertical direction
  8019. * @private
  8020. */
  8021. Node.prototype._addForce = function(fx, fy) {
  8022. this.fx += fx;
  8023. this.fy += fy;
  8024. };
  8025. /**
  8026. * Perform one discrete step for the node
  8027. * @param {number} interval Time interval in seconds
  8028. */
  8029. Node.prototype.discreteStep = function(interval) {
  8030. if (!this.xFixed) {
  8031. var dx = -this.damping * this.vx; // damping force
  8032. var ax = (this.fx + dx) / this.mass; // acceleration
  8033. this.vx += ax / interval; // velocity
  8034. this.x += this.vx / interval; // position
  8035. }
  8036. if (!this.yFixed) {
  8037. var dy = -this.damping * this.vy; // damping force
  8038. var ay = (this.fy + dy) / this.mass; // acceleration
  8039. this.vy += ay / interval; // velocity
  8040. this.y += this.vy / interval; // position
  8041. }
  8042. };
  8043. /**
  8044. * Check if this node has a fixed x and y position
  8045. * @return {boolean} true if fixed, false if not
  8046. */
  8047. Node.prototype.isFixed = function() {
  8048. return (this.xFixed && this.yFixed);
  8049. };
  8050. /**
  8051. * Check if this node is moving
  8052. * @param {number} vmin the minimum velocity considered as "moving"
  8053. * @return {boolean} true if moving, false if it has no velocity
  8054. */
  8055. // TODO: replace this method with calculating the kinetic energy
  8056. Node.prototype.isMoving = function(vmin) {
  8057. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  8058. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  8059. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  8060. };
  8061. /**
  8062. * check if this node is selecte
  8063. * @return {boolean} selected True if node is selected, else false
  8064. */
  8065. Node.prototype.isSelected = function() {
  8066. return this.selected;
  8067. };
  8068. /**
  8069. * Retrieve the value of the node. Can be undefined
  8070. * @return {Number} value
  8071. */
  8072. Node.prototype.getValue = function() {
  8073. return this.value;
  8074. };
  8075. /**
  8076. * Calculate the distance from the nodes location to the given location (x,y)
  8077. * @param {Number} x
  8078. * @param {Number} y
  8079. * @return {Number} value
  8080. */
  8081. Node.prototype.getDistance = function(x, y) {
  8082. var dx = this.x - x,
  8083. dy = this.y - y;
  8084. return Math.sqrt(dx * dx + dy * dy);
  8085. };
  8086. /**
  8087. * Adjust the value range of the node. The node will adjust it's radius
  8088. * based on its value.
  8089. * @param {Number} min
  8090. * @param {Number} max
  8091. */
  8092. Node.prototype.setValueRange = function(min, max) {
  8093. if (!this.radiusFixed && this.value !== undefined) {
  8094. if (max == min) {
  8095. this.radius = (this.radiusMin + this.radiusMax) / 2;
  8096. }
  8097. else {
  8098. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  8099. this.radius = (this.value - min) * scale + this.radiusMin;
  8100. }
  8101. }
  8102. this.baseRadiusValue = this.radius;
  8103. };
  8104. /**
  8105. * Draw this node in the given canvas
  8106. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8107. * @param {CanvasRenderingContext2D} ctx
  8108. */
  8109. Node.prototype.draw = function(ctx) {
  8110. throw "Draw method not initialized for node";
  8111. };
  8112. /**
  8113. * Recalculate the size of this node in the given canvas
  8114. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8115. * @param {CanvasRenderingContext2D} ctx
  8116. */
  8117. Node.prototype.resize = function(ctx) {
  8118. throw "Resize method not initialized for node";
  8119. };
  8120. /**
  8121. * Check if this object is overlapping with the provided object
  8122. * @param {Object} obj an object with parameters left, top, right, bottom
  8123. * @return {boolean} True if location is located on node
  8124. */
  8125. Node.prototype.isOverlappingWith = function(obj) {
  8126. return (this.left < obj.right &&
  8127. this.left + this.width > obj.left &&
  8128. this.top < obj.bottom &&
  8129. this.top + this.height > obj.top);
  8130. };
  8131. Node.prototype._resizeImage = function (ctx) {
  8132. // TODO: pre calculate the image size
  8133. if (!this.width || !this.height) { // undefined or 0
  8134. var width, height;
  8135. if (this.value) {
  8136. this.radius = this.baseRadiusValue;
  8137. var scale = this.imageObj.height / this.imageObj.width;
  8138. if (scale !== undefined) {
  8139. width = this.radius || this.imageObj.width;
  8140. height = this.radius * scale || this.imageObj.height;
  8141. }
  8142. else {
  8143. width = 0;
  8144. height = 0;
  8145. }
  8146. }
  8147. else {
  8148. width = this.imageObj.width;
  8149. height = this.imageObj.height;
  8150. }
  8151. this.width = width;
  8152. this.height = height;
  8153. if (this.width && this.height) {
  8154. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  8155. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  8156. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  8157. }
  8158. }
  8159. };
  8160. Node.prototype._drawImage = function (ctx) {
  8161. this._resizeImage(ctx);
  8162. this.left = this.x - this.width / 2;
  8163. this.top = this.y - this.height / 2;
  8164. var yLabel;
  8165. if (this.imageObj.width != 0 ) {
  8166. // draw the shade
  8167. if (this.clusterSize > 1) {
  8168. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  8169. lineWidth *= this.graphScaleInv;
  8170. lineWidth = Math.min(0.2 * this.width,lineWidth);
  8171. ctx.globalAlpha = 0.5;
  8172. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  8173. }
  8174. ctx.globalAlpha = 1.0;
  8175. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  8176. yLabel = this.y + this.height / 2;
  8177. }
  8178. else {
  8179. // image still loading... just draw the label for now
  8180. yLabel = this.y;
  8181. }
  8182. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  8183. };
  8184. Node.prototype._resizeBox = function (ctx) {
  8185. if (!this.width) {
  8186. var margin = 5;
  8187. var textSize = this.getTextSize(ctx);
  8188. this.width = textSize.width + 2 * margin;
  8189. this.height = textSize.height + 2 * margin;
  8190. this.width += this.clusterSize * 0.5 * this.clusterSizeWidthFactor;
  8191. this.height += this.clusterSize * 0.5 * this.clusterSizeHeightFactor;
  8192. //this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  8193. }
  8194. };
  8195. Node.prototype._drawBox = function (ctx) {
  8196. this._resizeBox(ctx);
  8197. this.left = this.x - this.width / 2;
  8198. this.top = this.y - this.height / 2;
  8199. var clusterLineWidth = 2.5;
  8200. var selectionLineWidth = 2;
  8201. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8202. // draw the outer border
  8203. if (this.clusterSize > 1) {
  8204. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8205. ctx.lineWidth *= this.graphScaleInv;
  8206. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8207. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  8208. ctx.stroke();
  8209. }
  8210. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8211. ctx.lineWidth *= this.graphScaleInv;
  8212. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8213. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8214. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  8215. ctx.fill();
  8216. ctx.stroke();
  8217. this._label(ctx, this.label, this.x, this.y);
  8218. };
  8219. Node.prototype._resizeDatabase = function (ctx) {
  8220. if (!this.width) {
  8221. var margin = 5;
  8222. var textSize = this.getTextSize(ctx);
  8223. var size = textSize.width + 2 * margin;
  8224. this.width = size;
  8225. this.height = size;
  8226. // scaling used for clustering
  8227. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  8228. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  8229. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  8230. }
  8231. };
  8232. Node.prototype._drawDatabase = function (ctx) {
  8233. this._resizeDatabase(ctx);
  8234. this.left = this.x - this.width / 2;
  8235. this.top = this.y - this.height / 2;
  8236. var clusterLineWidth = 2.5;
  8237. var selectionLineWidth = 2;
  8238. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8239. // draw the outer border
  8240. if (this.clusterSize > 1) {
  8241. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8242. ctx.lineWidth *= this.graphScaleInv;
  8243. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8244. 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);
  8245. ctx.stroke();
  8246. }
  8247. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8248. ctx.lineWidth *= this.graphScaleInv;
  8249. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8250. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8251. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  8252. ctx.fill();
  8253. ctx.stroke();
  8254. this._label(ctx, this.label, this.x, this.y);
  8255. };
  8256. Node.prototype._resizeCircle = function (ctx) {
  8257. if (!this.width) {
  8258. var margin = 5;
  8259. var textSize = this.getTextSize(ctx);
  8260. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  8261. this.radius = diameter / 2;
  8262. this.width = diameter;
  8263. this.height = diameter;
  8264. // scaling used for clustering
  8265. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  8266. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  8267. this.radius += this.clusterSize * 0.5*this.clusterSizeRadiusFactor;
  8268. }
  8269. };
  8270. Node.prototype._drawCircle = function (ctx) {
  8271. this._resizeCircle(ctx);
  8272. this.left = this.x - this.width / 2;
  8273. this.top = this.y - this.height / 2;
  8274. var clusterLineWidth = 2.5;
  8275. var selectionLineWidth = 2;
  8276. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8277. // draw the outer border
  8278. if (this.clusterSize > 1) {
  8279. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8280. ctx.lineWidth *= this.graphScaleInv;
  8281. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8282. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  8283. ctx.stroke();
  8284. }
  8285. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8286. ctx.lineWidth *= this.graphScaleInv;
  8287. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8288. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8289. ctx.circle(this.x, this.y, this.radius);
  8290. ctx.fill();
  8291. ctx.stroke();
  8292. this._label(ctx, this.label, this.x, this.y);
  8293. };
  8294. Node.prototype._resizeEllipse = function (ctx) {
  8295. if (!this.width) {
  8296. var textSize = this.getTextSize(ctx);
  8297. this.width = textSize.width * 1.5;
  8298. this.height = textSize.height * 2;
  8299. if (this.width < this.height) {
  8300. this.width = this.height;
  8301. }
  8302. // scaling used for clustering
  8303. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  8304. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  8305. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  8306. }
  8307. };
  8308. Node.prototype._drawEllipse = function (ctx) {
  8309. this._resizeEllipse(ctx);
  8310. this.left = this.x - this.width / 2;
  8311. this.top = this.y - this.height / 2;
  8312. var clusterLineWidth = 2.5;
  8313. var selectionLineWidth = 2;
  8314. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8315. // draw the outer border
  8316. if (this.clusterSize > 1) {
  8317. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8318. ctx.lineWidth *= this.graphScaleInv;
  8319. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8320. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  8321. ctx.stroke();
  8322. }
  8323. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8324. ctx.lineWidth *= this.graphScaleInv;
  8325. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8326. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8327. ctx.ellipse(this.left, this.top, this.width, this.height);
  8328. ctx.fill();
  8329. ctx.stroke();
  8330. this._label(ctx, this.label, this.x, this.y);
  8331. };
  8332. Node.prototype._drawDot = function (ctx) {
  8333. this._drawShape(ctx, 'circle');
  8334. };
  8335. Node.prototype._drawTriangle = function (ctx) {
  8336. this._drawShape(ctx, 'triangle');
  8337. };
  8338. Node.prototype._drawTriangleDown = function (ctx) {
  8339. this._drawShape(ctx, 'triangleDown');
  8340. };
  8341. Node.prototype._drawSquare = function (ctx) {
  8342. this._drawShape(ctx, 'square');
  8343. };
  8344. Node.prototype._drawStar = function (ctx) {
  8345. this._drawShape(ctx, 'star');
  8346. };
  8347. Node.prototype._resizeShape = function (ctx) {
  8348. if (!this.width) {
  8349. this.radius = this.baseRadiusValue;
  8350. var size = 2 * this.radius;
  8351. this.width = size;
  8352. this.height = size;
  8353. // scaling used for clustering
  8354. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  8355. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  8356. this.radius += this.clusterSize * 0.5 * this.clusterSizeRadiusFactor;
  8357. }
  8358. };
  8359. Node.prototype._drawShape = function (ctx, shape) {
  8360. this._resizeShape(ctx);
  8361. this.left = this.x - this.width / 2;
  8362. this.top = this.y - this.height / 2;
  8363. var clusterLineWidth = 2.5;
  8364. var selectionLineWidth = 2;
  8365. var radiusMultiplier = 2;
  8366. // choose draw method depending on the shape
  8367. switch (shape) {
  8368. case 'dot': radiusMultiplier = 2; break;
  8369. case 'square': radiusMultiplier = 2; break;
  8370. case 'triangle': radiusMultiplier = 3; break;
  8371. case 'triangleDown': radiusMultiplier = 3; break;
  8372. case 'star': radiusMultiplier = 4; break;
  8373. }
  8374. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  8375. // draw the outer border
  8376. if (this.clusterSize > 1) {
  8377. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8378. ctx.lineWidth *= this.graphScaleInv;
  8379. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8380. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  8381. ctx.stroke();
  8382. }
  8383. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  8384. ctx.lineWidth *= this.graphScaleInv;
  8385. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  8386. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  8387. ctx[shape](this.x, this.y, this.radius);
  8388. ctx.fill();
  8389. ctx.stroke();
  8390. if (this.label) {
  8391. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  8392. }
  8393. };
  8394. Node.prototype._resizeText = function (ctx) {
  8395. if (!this.width) {
  8396. var margin = 5;
  8397. var textSize = this.getTextSize(ctx);
  8398. this.width = textSize.width + 2 * margin;
  8399. this.height = textSize.height + 2 * margin;
  8400. // scaling used for clustering
  8401. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  8402. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  8403. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  8404. }
  8405. };
  8406. Node.prototype._drawText = function (ctx) {
  8407. this._resizeText(ctx);
  8408. this.left = this.x - this.width / 2;
  8409. this.top = this.y - this.height / 2;
  8410. this._label(ctx, this.label, this.x, this.y);
  8411. };
  8412. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  8413. if (text) {
  8414. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8415. ctx.fillStyle = this.fontColor || "black";
  8416. ctx.textAlign = align || "center";
  8417. ctx.textBaseline = baseline || "middle";
  8418. var lines = text.split('\n'),
  8419. lineCount = lines.length,
  8420. fontSize = (this.fontSize + 4),
  8421. yLine = y + (1 - lineCount) / 2 * fontSize;
  8422. for (var i = 0; i < lineCount; i++) {
  8423. ctx.fillText(lines[i], x, yLine);
  8424. yLine += fontSize;
  8425. }
  8426. }
  8427. };
  8428. Node.prototype.getTextSize = function(ctx) {
  8429. if (this.label !== undefined) {
  8430. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  8431. var lines = this.label.split('\n'),
  8432. height = (this.fontSize + 4) * lines.length,
  8433. width = 0;
  8434. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  8435. width = Math.max(width, ctx.measureText(lines[i]).width);
  8436. }
  8437. return {"width": width, "height": height};
  8438. }
  8439. else {
  8440. return {"width": 0, "height": 0};
  8441. }
  8442. };
  8443. /**
  8444. * this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
  8445. * there is a safety margin of 0.3 * width;
  8446. *
  8447. * @returns {boolean}
  8448. */
  8449. Node.prototype.inArea = function() {
  8450. if (this.width !== undefined) {
  8451. return (this.x + this.width*0.8 >= this.canvasTopLeft.x &&
  8452. this.x - this.width*0.8 < this.canvasBottomRight.x &&
  8453. this.y + this.height*0.8 >= this.canvasTopLeft.y &&
  8454. this.y - this.height*0.8 < this.canvasBottomRight.y);
  8455. }
  8456. else {
  8457. return true;
  8458. }
  8459. }
  8460. /**
  8461. * checks if the core of the node is in the display area, this is used for opening clusters around zoom
  8462. * @returns {boolean}
  8463. */
  8464. Node.prototype.inView = function() {
  8465. return (this.x >= this.canvasTopLeft.x &&
  8466. this.x < this.canvasBottomRight.x &&
  8467. this.y >= this.canvasTopLeft.y &&
  8468. this.y < this.canvasBottomRight.y);
  8469. }
  8470. /**
  8471. * This allows the zoom level of the graph to influence the rendering
  8472. * We store the inverted scale and the coordinates of the top left, and bottom right points of the canvas
  8473. *
  8474. * @param scale
  8475. * @param canvasTopLeft
  8476. * @param canvasBottomRight
  8477. */
  8478. Node.prototype.setScaleAndPos = function(scale,canvasTopLeft,canvasBottomRight) {
  8479. this.graphScaleInv = 1.0/scale;
  8480. this.canvasTopLeft = canvasTopLeft;
  8481. this.canvasBottomRight = canvasBottomRight;
  8482. };
  8483. /**
  8484. * This allows the zoom level of the graph to influence the rendering
  8485. *
  8486. * @param scale
  8487. */
  8488. Node.prototype.setScale = function(scale) {
  8489. this.graphScaleInv = 1.0/scale;
  8490. };
  8491. /**
  8492. * This function updates the damping parameter for clusters, based ont he
  8493. *
  8494. * @param {Integer} numberOfNodes
  8495. */
  8496. Node.prototype.updateDamping = function(numberOfNodes) {
  8497. this.damping = 0.8 + 0.1*this.clusterSize * (1 + 2/Math.pow(numberOfNodes,2));
  8498. };
  8499. /**
  8500. * set the velocity at 0. Is called when this node is contained in another during clustering
  8501. */
  8502. Node.prototype.clearVelocity = function() {
  8503. this.vx = 0;
  8504. this.vy = 0;
  8505. };
  8506. /**
  8507. * Basic preservation of (kinectic) energy
  8508. *
  8509. * @param massBeforeClustering
  8510. */
  8511. Node.prototype.updateVelocity = function(massBeforeClustering) {
  8512. var energyBefore = this.vx * this.vx * massBeforeClustering;
  8513. this.vx = Math.sqrt(energyBefore/this.mass);
  8514. energyBefore = this.vy * this.vy * massBeforeClustering;
  8515. this.vy = Math.sqrt(energyBefore/this.mass);
  8516. };
  8517. /**
  8518. * @class Edge
  8519. *
  8520. * A edge connects two nodes
  8521. * @param {Object} properties Object with properties. Must contain
  8522. * At least properties from and to.
  8523. * Available properties: from (number),
  8524. * to (number), label (string, color (string),
  8525. * width (number), style (string),
  8526. * length (number), title (string)
  8527. * @param {Graph} graph A graph object, used to find and edge to
  8528. * nodes.
  8529. * @param {Object} constants An object with default values for
  8530. * example for the color
  8531. */
  8532. function Edge (properties, graph, constants) {
  8533. if (!graph) {
  8534. throw "No graph provided";
  8535. }
  8536. this.graph = graph;
  8537. // initialize constants
  8538. this.widthMin = constants.edges.widthMin;
  8539. this.widthMax = constants.edges.widthMax;
  8540. // initialize variables
  8541. this.id = undefined;
  8542. this.fromId = undefined;
  8543. this.toId = undefined;
  8544. this.style = constants.edges.style;
  8545. this.title = undefined;
  8546. this.width = constants.edges.width;
  8547. this.value = undefined;
  8548. this.length = constants.edges.length;
  8549. this.from = null; // a node
  8550. this.to = null; // a node
  8551. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  8552. // by storing the original information we can revert to the original connection when the cluser is opened.
  8553. this.originalFromID = [];
  8554. this.originalToID = [];
  8555. this.connected = false;
  8556. // Added to support dashed lines
  8557. // David Jordan
  8558. // 2012-08-08
  8559. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  8560. this.stiffness = undefined; // depends on the length of the edge
  8561. this.color = constants.edges.color;
  8562. this.widthFixed = false;
  8563. this.lengthFixed = false;
  8564. this.setProperties(properties, constants);
  8565. };
  8566. /**
  8567. * Set or overwrite properties for the edge
  8568. * @param {Object} properties an object with properties
  8569. * @param {Object} constants and object with default, global properties
  8570. */
  8571. Edge.prototype.setProperties = function(properties, constants) {
  8572. if (!properties) {
  8573. return;
  8574. }
  8575. if (properties.from != undefined) {this.fromId = properties.from;}
  8576. if (properties.to != undefined) {this.toId = properties.to;}
  8577. if (properties.id != undefined) {this.id = properties.id;}
  8578. if (properties.style != undefined) {this.style = properties.style;}
  8579. if (properties.label != undefined) {this.label = properties.label;}
  8580. if (this.label) {
  8581. this.fontSize = constants.edges.fontSize;
  8582. this.fontFace = constants.edges.fontFace;
  8583. this.fontColor = constants.edges.fontColor;
  8584. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  8585. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  8586. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  8587. }
  8588. if (properties.title != undefined) {this.title = properties.title;}
  8589. if (properties.width != undefined) {this.width = properties.width;}
  8590. if (properties.value != undefined) {this.value = properties.value;}
  8591. if (properties.length != undefined) {this.length = properties.length;}
  8592. // Added to support dashed lines
  8593. // David Jordan
  8594. // 2012-08-08
  8595. if (properties.dash) {
  8596. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  8597. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  8598. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  8599. }
  8600. if (properties.color != undefined) {this.color = properties.color;}
  8601. // A node is connected when it has a from and to node.
  8602. this.connect();
  8603. this.widthFixed = this.widthFixed || (properties.width != undefined);
  8604. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  8605. this.stiffness = 1 / this.length;
  8606. // set draw method based on style
  8607. switch (this.style) {
  8608. case 'line': this.draw = this._drawLine; break;
  8609. case 'arrow': this.draw = this._drawArrow; break;
  8610. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  8611. case 'dash-line': this.draw = this._drawDashLine; break;
  8612. default: this.draw = this._drawLine; break;
  8613. }
  8614. };
  8615. /**
  8616. * Connect an edge to its nodes
  8617. */
  8618. Edge.prototype.connect = function () {
  8619. this.disconnect();
  8620. this.from = this.graph.nodes[this.fromId] || null;
  8621. this.to = this.graph.nodes[this.toId] || null;
  8622. this.connected = (this.from && this.to);
  8623. if (this.connected) {
  8624. this.from.attachEdge(this);
  8625. this.to.attachEdge(this);
  8626. }
  8627. else {
  8628. if (this.from) {
  8629. this.from.detachEdge(this);
  8630. }
  8631. if (this.to) {
  8632. this.to.detachEdge(this);
  8633. }
  8634. }
  8635. };
  8636. /**
  8637. * Disconnect an edge from its nodes
  8638. */
  8639. Edge.prototype.disconnect = function () {
  8640. if (this.from) {
  8641. this.from.detachEdge(this);
  8642. this.from = null;
  8643. }
  8644. if (this.to) {
  8645. this.to.detachEdge(this);
  8646. this.to = null;
  8647. }
  8648. this.connected = false;
  8649. };
  8650. /**
  8651. * get the title of this edge.
  8652. * @return {string} title The title of the edge, or undefined when no title
  8653. * has been set.
  8654. */
  8655. Edge.prototype.getTitle = function() {
  8656. return this.title;
  8657. };
  8658. /**
  8659. * Retrieve the value of the edge. Can be undefined
  8660. * @return {Number} value
  8661. */
  8662. Edge.prototype.getValue = function() {
  8663. return this.value;
  8664. };
  8665. /**
  8666. * Adjust the value range of the edge. The edge will adjust it's width
  8667. * based on its value.
  8668. * @param {Number} min
  8669. * @param {Number} max
  8670. */
  8671. Edge.prototype.setValueRange = function(min, max) {
  8672. if (!this.widthFixed && this.value !== undefined) {
  8673. var scale = (this.widthMax - this.widthMin) / (max - min);
  8674. this.width = (this.value - min) * scale + this.widthMin;
  8675. }
  8676. };
  8677. /**
  8678. * Redraw a edge
  8679. * Draw this edge in the given canvas
  8680. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8681. * @param {CanvasRenderingContext2D} ctx
  8682. */
  8683. Edge.prototype.draw = function(ctx) {
  8684. throw "Method draw not initialized in edge";
  8685. };
  8686. /**
  8687. * Check if this object is overlapping with the provided object
  8688. * @param {Object} obj an object with parameters left, top
  8689. * @return {boolean} True if location is located on the edge
  8690. */
  8691. Edge.prototype.isOverlappingWith = function(obj) {
  8692. var distMax = 10;
  8693. var xFrom = this.from.x;
  8694. var yFrom = this.from.y;
  8695. var xTo = this.to.x;
  8696. var yTo = this.to.y;
  8697. var xObj = obj.left;
  8698. var yObj = obj.top;
  8699. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  8700. return (dist < distMax);
  8701. };
  8702. /**
  8703. * Redraw a edge as a line
  8704. * Draw this edge in the given canvas
  8705. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8706. * @param {CanvasRenderingContext2D} ctx
  8707. * @private
  8708. */
  8709. Edge.prototype._drawLine = function(ctx) {
  8710. // set style
  8711. ctx.strokeStyle = this.color;
  8712. ctx.lineWidth = this._getLineWidth();
  8713. var point;
  8714. if (this.from != this.to) {
  8715. // draw line
  8716. this._line(ctx);
  8717. // draw label
  8718. if (this.label) {
  8719. point = this._pointOnLine(0.5);
  8720. this._label(ctx, this.label, point.x, point.y);
  8721. }
  8722. }
  8723. else {
  8724. var x, y;
  8725. var radius = this.length / 4;
  8726. var node = this.from;
  8727. if (!node.width) {
  8728. node.resize(ctx);
  8729. }
  8730. if (node.width > node.height) {
  8731. x = node.x + node.width / 2;
  8732. y = node.y - radius;
  8733. }
  8734. else {
  8735. x = node.x + radius;
  8736. y = node.y - node.height / 2;
  8737. }
  8738. this._circle(ctx, x, y, radius);
  8739. point = this._pointOnCircle(x, y, radius, 0.5);
  8740. this._label(ctx, this.label, point.x, point.y);
  8741. }
  8742. };
  8743. /**
  8744. * Get the line width of the edge. Depends on width and whether one of the
  8745. * connected nodes is selected.
  8746. * @return {Number} width
  8747. * @private
  8748. */
  8749. Edge.prototype._getLineWidth = function() {
  8750. if (this.from.selected || this.to.selected) {
  8751. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  8752. }
  8753. else {
  8754. return this.width*this.graphScaleInv;
  8755. }
  8756. };
  8757. /**
  8758. * Draw a line between two nodes
  8759. * @param {CanvasRenderingContext2D} ctx
  8760. * @private
  8761. */
  8762. Edge.prototype._line = function (ctx) {
  8763. // draw a straight line
  8764. ctx.beginPath();
  8765. ctx.moveTo(this.from.x, this.from.y);
  8766. ctx.lineTo(this.to.x, this.to.y);
  8767. ctx.stroke();
  8768. };
  8769. /**
  8770. * Draw a line from a node to itself, a circle
  8771. * @param {CanvasRenderingContext2D} ctx
  8772. * @param {Number} x
  8773. * @param {Number} y
  8774. * @param {Number} radius
  8775. * @private
  8776. */
  8777. Edge.prototype._circle = function (ctx, x, y, radius) {
  8778. // draw a circle
  8779. ctx.beginPath();
  8780. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  8781. ctx.stroke();
  8782. };
  8783. /**
  8784. * Draw label with white background and with the middle at (x, y)
  8785. * @param {CanvasRenderingContext2D} ctx
  8786. * @param {String} text
  8787. * @param {Number} x
  8788. * @param {Number} y
  8789. * @private
  8790. */
  8791. Edge.prototype._label = function (ctx, text, x, y) {
  8792. if (text) {
  8793. // TODO: cache the calculated size
  8794. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  8795. this.fontSize + "px " + this.fontFace;
  8796. ctx.fillStyle = 'white';
  8797. var width = ctx.measureText(text).width;
  8798. var height = this.fontSize;
  8799. var left = x - width / 2;
  8800. var top = y - height / 2;
  8801. ctx.fillRect(left, top, width, height);
  8802. // draw text
  8803. ctx.fillStyle = this.fontColor || "black";
  8804. ctx.textAlign = "left";
  8805. ctx.textBaseline = "top";
  8806. ctx.fillText(text, left, top);
  8807. }
  8808. };
  8809. /**
  8810. * Redraw a edge as a dashed line
  8811. * Draw this edge in the given canvas
  8812. * @author David Jordan
  8813. * @date 2012-08-08
  8814. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8815. * @param {CanvasRenderingContext2D} ctx
  8816. * @private
  8817. */
  8818. Edge.prototype._drawDashLine = function(ctx) {
  8819. // set style
  8820. ctx.strokeStyle = this.color;
  8821. ctx.lineWidth = this._getLineWidth();
  8822. // draw dashed line
  8823. ctx.beginPath();
  8824. ctx.lineCap = 'round';
  8825. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  8826. {
  8827. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8828. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  8829. }
  8830. 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
  8831. {
  8832. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  8833. [this.dash.length,this.dash.gap]);
  8834. }
  8835. else //If all else fails draw a line
  8836. {
  8837. ctx.moveTo(this.from.x, this.from.y);
  8838. ctx.lineTo(this.to.x, this.to.y);
  8839. }
  8840. ctx.stroke();
  8841. // draw label
  8842. if (this.label) {
  8843. var point = this._pointOnLine(0.5);
  8844. this._label(ctx, this.label, point.x, point.y);
  8845. }
  8846. };
  8847. /**
  8848. * Get a point on a line
  8849. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8850. * @return {Object} point
  8851. * @private
  8852. */
  8853. Edge.prototype._pointOnLine = function (percentage) {
  8854. return {
  8855. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  8856. y: (1 - percentage) * this.from.y + percentage * this.to.y
  8857. }
  8858. };
  8859. /**
  8860. * Get a point on a circle
  8861. * @param {Number} x
  8862. * @param {Number} y
  8863. * @param {Number} radius
  8864. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  8865. * @return {Object} point
  8866. * @private
  8867. */
  8868. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  8869. var angle = (percentage - 3/8) * 2 * Math.PI;
  8870. return {
  8871. x: x + radius * Math.cos(angle),
  8872. y: y - radius * Math.sin(angle)
  8873. }
  8874. };
  8875. /**
  8876. * Redraw a edge as a line with an arrow halfway the line
  8877. * Draw this edge in the given canvas
  8878. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8879. * @param {CanvasRenderingContext2D} ctx
  8880. * @private
  8881. */
  8882. Edge.prototype._drawArrowCenter = function(ctx) {
  8883. var point;
  8884. // set style
  8885. ctx.strokeStyle = this.color;
  8886. ctx.fillStyle = this.color;
  8887. ctx.lineWidth = this._getLineWidth();
  8888. if (this.from != this.to) {
  8889. // draw line
  8890. this._line(ctx);
  8891. // draw an arrow halfway the line
  8892. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8893. var length = 10 + 5 * this.width; // TODO: make customizable?
  8894. point = this._pointOnLine(0.5);
  8895. ctx.arrow(point.x, point.y, angle, length);
  8896. ctx.fill();
  8897. ctx.stroke();
  8898. // draw label
  8899. if (this.label) {
  8900. point = this._pointOnLine(0.5);
  8901. this._label(ctx, this.label, point.x, point.y);
  8902. }
  8903. }
  8904. else {
  8905. // draw circle
  8906. var x, y;
  8907. var radius = this.length / 4;
  8908. var node = this.from;
  8909. if (!node.width) {
  8910. node.resize(ctx);
  8911. }
  8912. if (node.width > node.height) {
  8913. x = node.x + node.width / 2;
  8914. y = node.y - radius;
  8915. }
  8916. else {
  8917. x = node.x + radius;
  8918. y = node.y - node.height / 2;
  8919. }
  8920. this._circle(ctx, x, y, radius);
  8921. // draw all arrows
  8922. var angle = 0.2 * Math.PI;
  8923. var length = 10 + 5 * this.width; // TODO: make customizable?
  8924. point = this._pointOnCircle(x, y, radius, 0.5);
  8925. ctx.arrow(point.x, point.y, angle, length);
  8926. ctx.fill();
  8927. ctx.stroke();
  8928. // draw label
  8929. if (this.label) {
  8930. point = this._pointOnCircle(x, y, radius, 0.5);
  8931. this._label(ctx, this.label, point.x, point.y);
  8932. }
  8933. }
  8934. };
  8935. /**
  8936. * Redraw a edge as a line with an arrow
  8937. * Draw this edge in the given canvas
  8938. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  8939. * @param {CanvasRenderingContext2D} ctx
  8940. * @private
  8941. */
  8942. Edge.prototype._drawArrow = function(ctx) {
  8943. // set style
  8944. ctx.strokeStyle = this.color;
  8945. ctx.fillStyle = this.color;
  8946. ctx.lineWidth = this._getLineWidth();
  8947. // draw line
  8948. var angle, length;
  8949. if (this.from != this.to) {
  8950. // calculate length and angle of the line
  8951. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  8952. var dx = (this.to.x - this.from.x);
  8953. var dy = (this.to.y - this.from.y);
  8954. var lEdge = Math.sqrt(dx * dx + dy * dy);
  8955. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  8956. var pFrom = (lEdge - lFrom) / lEdge;
  8957. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  8958. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  8959. var lTo = this.to.distanceToBorder(ctx, angle);
  8960. var pTo = (lEdge - lTo) / lEdge;
  8961. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  8962. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  8963. ctx.beginPath();
  8964. ctx.moveTo(xFrom, yFrom);
  8965. ctx.lineTo(xTo, yTo);
  8966. ctx.stroke();
  8967. // draw arrow at the end of the line
  8968. length = 10 + 5 * this.width; // TODO: make customizable?
  8969. ctx.arrow(xTo, yTo, angle, length);
  8970. ctx.fill();
  8971. ctx.stroke();
  8972. // draw label
  8973. if (this.label) {
  8974. var point = this._pointOnLine(0.5);
  8975. this._label(ctx, this.label, point.x, point.y);
  8976. }
  8977. }
  8978. else {
  8979. // draw circle
  8980. var node = this.from;
  8981. var x, y, arrow;
  8982. var radius = this.length / 4;
  8983. if (!node.width) {
  8984. node.resize(ctx);
  8985. }
  8986. if (node.width > node.height) {
  8987. x = node.x + node.width / 2;
  8988. y = node.y - radius;
  8989. arrow = {
  8990. x: x,
  8991. y: node.y,
  8992. angle: 0.9 * Math.PI
  8993. };
  8994. }
  8995. else {
  8996. x = node.x + radius;
  8997. y = node.y - node.height / 2;
  8998. arrow = {
  8999. x: node.x,
  9000. y: y,
  9001. angle: 0.6 * Math.PI
  9002. };
  9003. }
  9004. ctx.beginPath();
  9005. // TODO: do not draw a circle, but an arc
  9006. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  9007. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  9008. ctx.stroke();
  9009. // draw all arrows
  9010. length = 10 + 5 * this.width; // TODO: make customizable?
  9011. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  9012. ctx.fill();
  9013. ctx.stroke();
  9014. // draw label
  9015. if (this.label) {
  9016. point = this._pointOnCircle(x, y, radius, 0.5);
  9017. this._label(ctx, this.label, point.x, point.y);
  9018. }
  9019. }
  9020. };
  9021. /**
  9022. * Calculate the distance between a point (x3,y3) and a line segment from
  9023. * (x1,y1) to (x2,y2).
  9024. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  9025. * @param {number} x1
  9026. * @param {number} y1
  9027. * @param {number} x2
  9028. * @param {number} y2
  9029. * @param {number} x3
  9030. * @param {number} y3
  9031. * @private
  9032. */
  9033. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  9034. var px = x2-x1,
  9035. py = y2-y1,
  9036. something = px*px + py*py,
  9037. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  9038. if (u > 1) {
  9039. u = 1;
  9040. }
  9041. else if (u < 0) {
  9042. u = 0;
  9043. }
  9044. var x = x1 + u * px,
  9045. y = y1 + u * py,
  9046. dx = x - x3,
  9047. dy = y - y3;
  9048. //# Note: If the actual distance does not matter,
  9049. //# if you only want to compare what this function
  9050. //# returns to other results of this function, you
  9051. //# can just return the squared distance instead
  9052. //# (i.e. remove the sqrt) to gain a little performance
  9053. return Math.sqrt(dx*dx + dy*dy);
  9054. };
  9055. /**
  9056. * This allows the zoom level of the graph to influence the rendering
  9057. *
  9058. * @param scale
  9059. */
  9060. Edge.prototype.setScale = function(scale) {
  9061. this.graphScaleInv = 1.0/scale;
  9062. };
  9063. /**
  9064. * Popup is a class to create a popup window with some text
  9065. * @param {Element} container The container object.
  9066. * @param {Number} [x]
  9067. * @param {Number} [y]
  9068. * @param {String} [text]
  9069. */
  9070. function Popup(container, x, y, text) {
  9071. if (container) {
  9072. this.container = container;
  9073. }
  9074. else {
  9075. this.container = document.body;
  9076. }
  9077. this.x = 0;
  9078. this.y = 0;
  9079. this.padding = 5;
  9080. if (x !== undefined && y !== undefined ) {
  9081. this.setPosition(x, y);
  9082. }
  9083. if (text !== undefined) {
  9084. this.setText(text);
  9085. }
  9086. // create the frame
  9087. this.frame = document.createElement("div");
  9088. var style = this.frame.style;
  9089. style.position = "absolute";
  9090. style.visibility = "hidden";
  9091. style.border = "1px solid #666";
  9092. style.color = "black";
  9093. style.padding = this.padding + "px";
  9094. style.backgroundColor = "#FFFFC6";
  9095. style.borderRadius = "3px";
  9096. style.MozBorderRadius = "3px";
  9097. style.WebkitBorderRadius = "3px";
  9098. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  9099. style.whiteSpace = "nowrap";
  9100. this.container.appendChild(this.frame);
  9101. };
  9102. /**
  9103. * @param {number} x Horizontal position of the popup window
  9104. * @param {number} y Vertical position of the popup window
  9105. */
  9106. Popup.prototype.setPosition = function(x, y) {
  9107. this.x = parseInt(x);
  9108. this.y = parseInt(y);
  9109. };
  9110. /**
  9111. * Set the text for the popup window. This can be HTML code
  9112. * @param {string} text
  9113. */
  9114. Popup.prototype.setText = function(text) {
  9115. this.frame.innerHTML = text;
  9116. };
  9117. /**
  9118. * Show the popup window
  9119. * @param {boolean} show Optional. Show or hide the window
  9120. */
  9121. Popup.prototype.show = function (show) {
  9122. if (show === undefined) {
  9123. show = true;
  9124. }
  9125. if (show) {
  9126. var height = this.frame.clientHeight;
  9127. var width = this.frame.clientWidth;
  9128. var maxHeight = this.frame.parentNode.clientHeight;
  9129. var maxWidth = this.frame.parentNode.clientWidth;
  9130. var top = (this.y - height);
  9131. if (top + height + this.padding > maxHeight) {
  9132. top = maxHeight - height - this.padding;
  9133. }
  9134. if (top < this.padding) {
  9135. top = this.padding;
  9136. }
  9137. var left = this.x;
  9138. if (left + width + this.padding > maxWidth) {
  9139. left = maxWidth - width - this.padding;
  9140. }
  9141. if (left < this.padding) {
  9142. left = this.padding;
  9143. }
  9144. this.frame.style.left = left + "px";
  9145. this.frame.style.top = top + "px";
  9146. this.frame.style.visibility = "visible";
  9147. }
  9148. else {
  9149. this.hide();
  9150. }
  9151. };
  9152. /**
  9153. * Hide the popup window
  9154. */
  9155. Popup.prototype.hide = function () {
  9156. this.frame.style.visibility = "hidden";
  9157. };
  9158. /**
  9159. * @class Groups
  9160. * This class can store groups and properties specific for groups.
  9161. */
  9162. Groups = function () {
  9163. this.clear();
  9164. this.defaultIndex = 0;
  9165. };
  9166. /**
  9167. * default constants for group colors
  9168. */
  9169. Groups.DEFAULT = [
  9170. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  9171. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  9172. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  9173. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  9174. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  9175. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  9176. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  9177. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  9178. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  9179. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  9180. ];
  9181. /**
  9182. * Clear all groups
  9183. */
  9184. Groups.prototype.clear = function () {
  9185. this.groups = {};
  9186. this.groups.length = function()
  9187. {
  9188. var i = 0;
  9189. for ( var p in this ) {
  9190. if (this.hasOwnProperty(p)) {
  9191. i++;
  9192. }
  9193. }
  9194. return i;
  9195. }
  9196. };
  9197. /**
  9198. * get group properties of a groupname. If groupname is not found, a new group
  9199. * is added.
  9200. * @param {*} groupname Can be a number, string, Date, etc.
  9201. * @return {Object} group The created group, containing all group properties
  9202. */
  9203. Groups.prototype.get = function (groupname) {
  9204. var group = this.groups[groupname];
  9205. if (group == undefined) {
  9206. // create new group
  9207. var index = this.defaultIndex % Groups.DEFAULT.length;
  9208. this.defaultIndex++;
  9209. group = {};
  9210. group.color = Groups.DEFAULT[index];
  9211. this.groups[groupname] = group;
  9212. }
  9213. return group;
  9214. };
  9215. /**
  9216. * Add a custom group style
  9217. * @param {String} groupname
  9218. * @param {Object} style An object containing borderColor,
  9219. * backgroundColor, etc.
  9220. * @return {Object} group The created group object
  9221. */
  9222. Groups.prototype.add = function (groupname, style) {
  9223. this.groups[groupname] = style;
  9224. if (style.color) {
  9225. style.color = Node.parseColor(style.color);
  9226. }
  9227. return style;
  9228. };
  9229. /**
  9230. * @class Images
  9231. * This class loads images and keeps them stored.
  9232. */
  9233. Images = function () {
  9234. this.images = {};
  9235. this.callback = undefined;
  9236. };
  9237. /**
  9238. * Set an onload callback function. This will be called each time an image
  9239. * is loaded
  9240. * @param {function} callback
  9241. */
  9242. Images.prototype.setOnloadCallback = function(callback) {
  9243. this.callback = callback;
  9244. };
  9245. /**
  9246. *
  9247. * @param {string} url Url of the image
  9248. * @return {Image} img The image object
  9249. */
  9250. Images.prototype.load = function(url) {
  9251. var img = this.images[url];
  9252. if (img == undefined) {
  9253. // create the image
  9254. var images = this;
  9255. img = new Image();
  9256. this.images[url] = img;
  9257. img.onload = function() {
  9258. if (images.callback) {
  9259. images.callback(this);
  9260. }
  9261. };
  9262. img.src = url;
  9263. }
  9264. return img;
  9265. };
  9266. var SectorMixin = {
  9267. /**
  9268. * This function is only called by the setData function of the Graph object.
  9269. * This loads the global references into the active sector. This initializes the sector.
  9270. *
  9271. * @private
  9272. */
  9273. _putDataInSector : function() {
  9274. this.sectors["active"][this._sector()].nodes = this.nodes;
  9275. this.sectors["active"][this._sector()].edges = this.edges;
  9276. this.sectors["active"][this._sector()].nodeIndices = this.nodeIndices;
  9277. },
  9278. /**
  9279. * /**
  9280. * This function sets the global references to nodes, edges and nodeIndices back to
  9281. * those of the supplied (active) sector. If a type is defined, do the specific type
  9282. *
  9283. * @param {String} sectorID
  9284. * @param {String} [sectorType] | "active" or "frozen"
  9285. * @private
  9286. */
  9287. _switchToSector : function(sectorID, sectorType) {
  9288. if (sectorType === undefined || sectorType == "active") {
  9289. this._switchToActiveSector(sectorID);
  9290. }
  9291. else {
  9292. this._switchToFrozenSector(sectorID);
  9293. }
  9294. },
  9295. /**
  9296. * This function sets the global references to nodes, edges and nodeIndices back to
  9297. * those of the supplied active sector.
  9298. *
  9299. * @param sectorID
  9300. * @private
  9301. */
  9302. _switchToActiveSector : function(sectorID) {
  9303. this.nodeIndices = this.sectors["active"][sectorID]["nodeIndices"];
  9304. this.nodes = this.sectors["active"][sectorID]["nodes"];
  9305. this.edges = this.sectors["active"][sectorID]["edges"];
  9306. },
  9307. /**
  9308. * This function sets the global references to nodes, edges and nodeIndices back to
  9309. * those of the supplied frozen sector.
  9310. *
  9311. * @param sectorID
  9312. * @private
  9313. */
  9314. _switchToFrozenSector : function(sectorID) {
  9315. this.nodeIndices = this.sectors["frozen"][sectorID]["nodeIndices"];
  9316. this.nodes = this.sectors["frozen"][sectorID]["nodes"];
  9317. this.edges = this.sectors["frozen"][sectorID]["edges"];
  9318. },
  9319. /**
  9320. * This function sets the global references to nodes, edges and nodeIndices back to
  9321. * those of the currently active sector.
  9322. *
  9323. * @private
  9324. */
  9325. _loadLatestSector : function() {
  9326. this._switchToSector(this._sector());
  9327. },
  9328. /**
  9329. * This function returns the currently active sector ID
  9330. *
  9331. * @returns {String}
  9332. * @private
  9333. */
  9334. _sector : function() {
  9335. return this.activeSector[this.activeSector.length-1];
  9336. },
  9337. /**
  9338. * This function returns the previously active sector ID
  9339. *
  9340. * @returns {String}
  9341. * @private
  9342. */
  9343. _previousSector : function() {
  9344. if (this.activeSector.length > 1) {
  9345. return this.activeSector[this.activeSector.length-2];
  9346. }
  9347. else {
  9348. throw new TypeError('there are not enough sectors in the this.activeSector array.');
  9349. return "";
  9350. }
  9351. },
  9352. /**
  9353. * We add the active sector at the end of the this.activeSector array
  9354. * This ensures it is the currently active sector returned by _sector() and it reaches the top
  9355. * of the activeSector stack. When we reverse our steps we move from the end to the beginning of this stack.
  9356. *
  9357. * @param newID
  9358. * @private
  9359. */
  9360. _setActiveSector : function(newID) {
  9361. this.activeSector.push(newID);
  9362. },
  9363. /**
  9364. * We remove the currently active sector id from the active sector stack. This happens when
  9365. * we reactivate the previously active sector
  9366. *
  9367. * @private
  9368. */
  9369. _forgetLastSector : function() {
  9370. this.activeSector.pop();
  9371. },
  9372. /**
  9373. * This function creates a new active sector with the supplied newID. This newID
  9374. * is the expanding node id.
  9375. *
  9376. * @param {String} newID | ID of the new active sector
  9377. * @private
  9378. */
  9379. _createNewSector : function(newID) {
  9380. // create the new sector
  9381. this.sectors["active"][newID] = {"nodes":{},
  9382. "edges":{},
  9383. "nodeIndices":[],
  9384. "formationScale": this.scale,
  9385. "drawingNode": undefined};
  9386. // create the new sector render node. This gives visual feedback that you are in a new sector.
  9387. this.sectors["active"][newID]['drawingNode'] = new Node(
  9388. {id:newID,
  9389. color: {
  9390. background: "#eaefef",
  9391. border: "495c5e"
  9392. }
  9393. },{},{},this.constants);
  9394. this.sectors["active"][newID]['drawingNode'].clusterSize = 2;
  9395. },
  9396. /**
  9397. * This function removes the currently active sector. This is called when we create a new
  9398. * active sector.
  9399. *
  9400. * @param {String} sectorID | ID of the active sector that will be removed
  9401. * @private
  9402. */
  9403. _deleteActiveSector : function(sectorID) {
  9404. delete this.sectors["active"][sectorID];
  9405. },
  9406. /**
  9407. * This function removes the currently active sector. This is called when we reactivate
  9408. * the previously active sector.
  9409. *
  9410. * @param {String} sectorID | ID of the active sector that will be removed
  9411. * @private
  9412. */
  9413. _deleteFrozenSector : function(sectorID) {
  9414. delete this.sectors["frozen"][sectorID];
  9415. },
  9416. /**
  9417. * Freezing an active sector means moving it from the "active" object to the "frozen" object.
  9418. * We copy the references, then delete the active entree.
  9419. *
  9420. * @param sectorID
  9421. * @private
  9422. */
  9423. _freezeSector : function(sectorID) {
  9424. // we move the set references from the active to the frozen stack.
  9425. this.sectors["frozen"][sectorID] = this.sectors["active"][sectorID];
  9426. // we have moved the sector data into the frozen set, we now remove it from the active set
  9427. this._deleteActiveSector(sectorID);
  9428. },
  9429. /**
  9430. * This is the reverse operation of _freezeSector. Activating means moving the sector from the "frozen"
  9431. * object to the "active" object.
  9432. *
  9433. * @param sectorID
  9434. * @private
  9435. */
  9436. _activateSector : function(sectorID) {
  9437. // we move the set references from the frozen to the active stack.
  9438. this.sectors["active"][sectorID] = this.sectors["frozen"][sectorID];
  9439. // we have moved the sector data into the active set, we now remove it from the frozen stack
  9440. this._deleteFrozenSector(sectorID);
  9441. },
  9442. /**
  9443. * This function merges the data from the currently active sector with a frozen sector. This is used
  9444. * in the process of reverting back to the previously active sector.
  9445. * The data that is placed in the frozen (the previously active) sector is the node that has been removed from it
  9446. * upon the creation of a new active sector.
  9447. *
  9448. * @param sectorID
  9449. * @private
  9450. */
  9451. _mergeThisWithFrozen : function(sectorID) {
  9452. // copy all nodes
  9453. for (var nodeID in this.nodes) {
  9454. if (this.nodes.hasOwnProperty(nodeID)) {
  9455. this.sectors["frozen"][sectorID]["nodes"][nodeID] = this.nodes[nodeID];
  9456. }
  9457. }
  9458. // copy all edges (if not fully clustered, else there are no edges)
  9459. for (var edgeID in this.edges) {
  9460. if (this.edges.hasOwnProperty(edgeID)) {
  9461. this.sectors["frozen"][sectorID]["edges"][edgeID] = this.edges[edgeID];
  9462. }
  9463. }
  9464. // merge the nodeIndices
  9465. for (var i = 0; i < this.nodeIndices.length; i++) {
  9466. this.sectors["frozen"][sectorID]["nodeIndices"].push(this.nodeIndices[i]);
  9467. }
  9468. },
  9469. /**
  9470. * This clusters the sector to one cluster. It was a single cluster before this process started so
  9471. * we revert to that state. The clusterToFit function with a maximum size of 1 node does this.
  9472. *
  9473. * @private
  9474. */
  9475. _collapseThisToSingleCluster : function() {
  9476. this.clusterToFit(1,false);
  9477. },
  9478. /**
  9479. * We create a new active sector from the node that we want to open.
  9480. *
  9481. * @param node
  9482. * @private
  9483. */
  9484. _addSector : function(node) {
  9485. // this is the currently active sector
  9486. var sector = this._sector();
  9487. // this should allow me to select nodes from a frozen set.
  9488. // TODO: after rewriting the selection function, have this working
  9489. if (this.sectors['active'][sector]["nodes"].hasOwnProperty(node.id)) {
  9490. console.log("the node is part of the active sector");
  9491. }
  9492. else {
  9493. console.log("I dont know what the fuck happened!!");
  9494. }
  9495. // when we switch to a new sector, we remove the node that will be expanded from the current nodes list.
  9496. delete this.nodes[node.id];
  9497. var unqiueIdentifier = util.randomUUID();
  9498. // we fully freeze the currently active sector
  9499. this._freezeSector(sector);
  9500. // we create a new active sector. This sector has the ID of the node to ensure uniqueness
  9501. this._createNewSector(unqiueIdentifier);
  9502. // we add the active sector to the sectors array to be able to revert these steps later on
  9503. this._setActiveSector(unqiueIdentifier);
  9504. // we redirect the global references to the new sector's references. this._sector() now returns unqiueIdentifier
  9505. this._switchToSector(this._sector());
  9506. // finally we add the node we removed from our previous active sector to the new active sector
  9507. this.nodes[node.id] = node;
  9508. },
  9509. /**
  9510. * We close the sector that is currently open and revert back to the one before.
  9511. * If the active sector is the "default" sector, nothing happens.
  9512. *
  9513. * @private
  9514. */
  9515. _collapseSector : function() {
  9516. // the currently active sector
  9517. var sector = this._sector();
  9518. // we cannot collapse the default sector
  9519. if (sector != "default") {
  9520. if ((this.nodeIndices.length == 1) ||
  9521. (this.sectors["active"][sector]["drawingNode"].width*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  9522. (this.sectors["active"][sector]["drawingNode"].height*this.scale < this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  9523. var previousSector = this._previousSector();
  9524. // we collapse the sector back to a single cluster
  9525. this._collapseThisToSingleCluster();
  9526. // we move the remaining nodes, edges and nodeIndices to the previous sector.
  9527. // This previous sector is the one we will reactivate
  9528. this._mergeThisWithFrozen(previousSector);
  9529. // the previously active (frozen) sector now has all the data from the currently active sector.
  9530. // we can now delete the active sector.
  9531. this._deleteActiveSector(sector);
  9532. // we activate the previously active (and currently frozen) sector.
  9533. this._activateSector(previousSector);
  9534. // we load the references from the newly active sector into the global references
  9535. this._switchToSector(previousSector);
  9536. // we forget the previously active sector because we reverted to the one before
  9537. this._forgetLastSector();
  9538. // finally, we update the node index list.
  9539. this._updateNodeIndexList();
  9540. }
  9541. }
  9542. },
  9543. /**
  9544. * This runs a function in all active sectors. This is used in _redraw() and the _calculateForces().
  9545. *
  9546. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  9547. * | we dont pass the function itself because then the "this" is the window object
  9548. * | instead of the Graph object
  9549. * @param {*} [args] | Optional: arguments to pass to the runFunction
  9550. * @private
  9551. */
  9552. _doInAllActiveSectors : function(runFunction,args) {
  9553. if (args === undefined) {
  9554. for (var sector in this.sectors["active"]) {
  9555. if (this.sectors["active"].hasOwnProperty(sector)) {
  9556. // switch the global references to those of this sector
  9557. this._switchToActiveSector(sector);
  9558. this[runFunction]();
  9559. }
  9560. }
  9561. }
  9562. else {
  9563. for (var sector in this.sectors["active"]) {
  9564. if (this.sectors["active"].hasOwnProperty(sector)) {
  9565. // switch the global references to those of this sector
  9566. this._switchToActiveSector(sector);
  9567. this[runFunction](args);
  9568. }
  9569. }
  9570. }
  9571. // we revert the global references back to our active sector
  9572. this._loadLatestSector();
  9573. },
  9574. /**
  9575. * This runs a function in all frozen sectors. This is used in the _redraw().
  9576. *
  9577. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  9578. * | we dont pass the function itself because then the "this" is the window object
  9579. * | instead of the Graph object
  9580. * @param {*} [args] | Optional: arguments to pass to the runFunction
  9581. * @private
  9582. */
  9583. _doInAllFrozenSectors : function(runFunction,args) {
  9584. if (args === undefined) {
  9585. for (var sector in this.sectors["frozen"]) {
  9586. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  9587. // switch the global references to those of this sector
  9588. this._switchToFrozenSector(sector);
  9589. this[runFunction]();
  9590. }
  9591. }
  9592. }
  9593. else {
  9594. for (var sector in this.sectors["frozen"]) {
  9595. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  9596. // switch the global references to those of this sector
  9597. this._switchToFrozenSector(sector);
  9598. this[runFunction](args);
  9599. }
  9600. }
  9601. }
  9602. this._loadLatestSector();
  9603. },
  9604. /**
  9605. * This runs a function in all sectors. This is used in the _redraw().
  9606. *
  9607. * @param {String} runFunction | This is the NAME of a function we want to call in all active sectors
  9608. * | we dont pass the function itself because then the "this" is the window object
  9609. * | instead of the Graph object
  9610. * @param {*} [args] | Optional: arguments to pass to the runFunction
  9611. * @private
  9612. */
  9613. _doInAllSectors : function(runFunction,argument) {
  9614. this._doInAllActiveSectors(runFunction,argument);
  9615. this._doInAllFrozenSectors(runFunction,argument);
  9616. },
  9617. /**
  9618. * This clears the nodeIndices list. We cannot use this.nodeIndices = [] because we would break the link with the
  9619. * active sector. Thus we clear the nodeIndices in the active sector, then reconnect the this.nodeIndices to it.
  9620. *
  9621. * @private
  9622. */
  9623. _clearNodeIndexList : function() {
  9624. var sector = this._sector();
  9625. this.sectors["active"][sector]["nodeIndices"] = [];
  9626. this.nodeIndices = this.sectors["active"][sector]["nodeIndices"];
  9627. },
  9628. /**
  9629. * Draw the encompassing sector node
  9630. *
  9631. * @param ctx
  9632. * @param sectorType
  9633. * @private
  9634. */
  9635. _drawSectorNodes : function(ctx,sectorType) {
  9636. var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node;
  9637. for (var sector in this.sectors[sectorType]) {
  9638. if (this.sectors[sectorType].hasOwnProperty(sector)) {
  9639. minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9;
  9640. if (this.sectors[sectorType][sector]["drawingNode"] !== undefined) {
  9641. this._switchToSector(sector,sectorType);
  9642. for (var nodeID in this.nodes) {
  9643. if (this.nodes.hasOwnProperty(nodeID)) {
  9644. node = this.nodes[nodeID];
  9645. node.resize(ctx);
  9646. if (minX > node.x - 0.5 * node.width) {minX = node.x - 0.5 * node.width;}
  9647. if (maxX < node.x + 0.5 * node.width) {maxX = node.x + 0.5 * node.width;}
  9648. if (minY > node.y - 0.5 * node.height) {minY = node.y - 0.5 * node.height;}
  9649. if (maxY < node.y + 0.5 * node.height) {maxY = node.y + 0.5 * node.height;}
  9650. }
  9651. }
  9652. node = this.sectors[sectorType][sector]["drawingNode"];
  9653. node.x = 0.5 * (maxX + minX);
  9654. node.y = 0.5 * (maxY + minY);
  9655. node.width = node.x - minX;
  9656. node.height = node.y - minY;
  9657. node.radius = Math.sqrt(Math.pow(node.width,2) + Math.pow(node.height,2));
  9658. node.setScale(this.scale);
  9659. node._drawCircle(ctx);
  9660. }
  9661. }
  9662. }
  9663. },
  9664. _drawAllSectorNodes : function(ctx) {
  9665. this._drawSectorNodes(ctx,"frozen");
  9666. this._drawSectorNodes(ctx,"active");
  9667. this._loadLatestSector();
  9668. }
  9669. };
  9670. /**
  9671. * @constructor Cluster
  9672. * Contains the cluster properties for the graph object
  9673. */
  9674. function Cluster() {
  9675. this.clusterSession = 0;
  9676. this.hubThreshold = 5;
  9677. }
  9678. /**
  9679. * This function clusters until the maxNumberOfNodes has been reached
  9680. *
  9681. * @param {Number} maxNumberOfNodes
  9682. * @param {Boolean} reposition
  9683. */
  9684. Cluster.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
  9685. var numberOfNodes = this.nodeIndices.length;
  9686. var maxLevels = 50;
  9687. var level = 0;
  9688. // we first cluster the hubs, then we pull in the outliers, repeat
  9689. while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
  9690. if (level % 3 == 0) {
  9691. console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession);
  9692. this.forceAggregateHubs();
  9693. }
  9694. else {
  9695. console.log("Pulling in Outliers @ level: ",level,"clusterSession",this.clusterSession);
  9696. this.increaseClusterLevel();
  9697. }
  9698. numberOfNodes = this.nodeIndices.length;
  9699. level += 1;
  9700. }
  9701. // after the clustering we reposition the nodes to reduce the initial chaos
  9702. if (level > 1 && reposition == true) {
  9703. this.repositionNodes();
  9704. }
  9705. };
  9706. /**
  9707. * This function can be called to open up a specific cluster. It is only called by
  9708. * It will unpack the cluster back one level.
  9709. *
  9710. * @param node | Node object: cluster to open.
  9711. */
  9712. Cluster.prototype.openCluster = function(node) {
  9713. var isMovingBeforeClustering = this.moving;
  9714. if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node)) {
  9715. this._addSector(node);
  9716. var level = 0;
  9717. while ((this.nodeIndices.length < this.constants.clustering.maxNumberOfNodes) &&
  9718. (level < 5)) {
  9719. this.decreaseClusterLevel();
  9720. level += 1;
  9721. }
  9722. }
  9723. else {
  9724. this._expandClusterNode(node,false,true);
  9725. // housekeeping
  9726. this._updateNodeIndexList();
  9727. this._updateDynamicEdges();
  9728. this.updateLabels();
  9729. }
  9730. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  9731. if (this.moving != isMovingBeforeClustering) {
  9732. this.start();
  9733. }
  9734. };
  9735. /**
  9736. * This calls the updateClustes with default arguments
  9737. */
  9738. Cluster.prototype.updateClustersDefault = function() {
  9739. if (this.constants.clustering.enableClustering) {
  9740. this.updateClusters(0,false,false);
  9741. }
  9742. };
  9743. /**
  9744. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  9745. * be clustered with their connected node. This can be repeated as many times as needed.
  9746. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  9747. */
  9748. Cluster.prototype.increaseClusterLevel = function() {
  9749. this.updateClusters(-1,false,true);
  9750. };
  9751. /**
  9752. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  9753. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  9754. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  9755. */
  9756. Cluster.prototype.decreaseClusterLevel = function() {
  9757. this.updateClusters(1,false,true);
  9758. };
  9759. /**
  9760. * This function clusters on zoom, it can be called with a predefined zoom direction
  9761. * If out, check if we can form clusters, if in, check if we can open clusters.
  9762. * This function is only called from _zoom()
  9763. *
  9764. * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
  9765. * @param {Boolean} recursive | enable or disable recursive calling of the opening of clusters
  9766. * @param {Boolean} force | enable or disable forcing
  9767. *
  9768. */
  9769. Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
  9770. var isMovingBeforeClustering = this.moving;
  9771. var amountOfNodes = this.nodeIndices.length;
  9772. // on zoom out collapse the sector back to default
  9773. if (this.previousScale > this.scale && zoomDirection == 0) {
  9774. this._collapseSector();
  9775. }
  9776. // check if we zoom in or out
  9777. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  9778. this._formClusters(force);
  9779. }
  9780. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom in
  9781. if (force == false) {
  9782. this._openClustersBySize();
  9783. }
  9784. else {
  9785. this._openClusters(recursive,force);
  9786. }
  9787. }
  9788. this._updateNodeIndexList();
  9789. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
  9790. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  9791. this._aggregateHubs(force);
  9792. this._updateNodeIndexList();
  9793. }
  9794. // we now reduce snakes.
  9795. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  9796. this.handleSnakes();
  9797. this._updateNodeIndexList();
  9798. }
  9799. this.previousScale = this.scale;
  9800. // rest of the housekeeping
  9801. this._updateDynamicEdges();
  9802. this.updateLabels();
  9803. // if a cluster was formed, we increase the clusterSession
  9804. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  9805. this.clusterSession += 1;
  9806. }
  9807. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  9808. if (this.moving != isMovingBeforeClustering) {
  9809. this.start();
  9810. }
  9811. };
  9812. /**
  9813. * This function handles the snakes. It is called on every updateClusters().
  9814. */
  9815. Cluster.prototype.handleSnakes = function() {
  9816. // after clustering we check how many snakes there are
  9817. var snakePercentage = this._getSnakeFraction();
  9818. if (snakePercentage > this.constants.clustering.snakeThreshold) {
  9819. this._reduceAmountOfSnakes(1 - this.constants.clustering.snakeThreshold / snakePercentage)
  9820. }
  9821. };
  9822. /**
  9823. * this functions starts clustering by hubs
  9824. * The minimum hub threshold is set globally
  9825. *
  9826. * @private
  9827. */
  9828. Cluster.prototype._aggregateHubs = function(force) {
  9829. this._getHubSize();
  9830. this._formClustersByHub(force,false);
  9831. };
  9832. /**
  9833. * This function is fired by keypress. It forces hubs to form.
  9834. *
  9835. */
  9836. Cluster.prototype.forceAggregateHubs = function() {
  9837. var isMovingBeforeClustering = this.moving;
  9838. var amountOfNodes = this.nodeIndices.length;
  9839. this._aggregateHubs(true);
  9840. // housekeeping
  9841. this._updateNodeIndexList();
  9842. this._updateDynamicEdges();
  9843. this.updateLabels();
  9844. // if a cluster was formed, we increase the clusterSession
  9845. if (this.nodeIndices.length != amountOfNodes) {
  9846. this.clusterSession += 1;
  9847. }
  9848. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  9849. if (this.moving != isMovingBeforeClustering) {
  9850. this.start();
  9851. }
  9852. };
  9853. /**
  9854. * If a cluster takes up more than a set percentage of the screen, open the cluster
  9855. *
  9856. * @private
  9857. */
  9858. Cluster.prototype._openClustersBySize = function() {
  9859. for (nodeID in this.nodes) {
  9860. if (this.nodes.hasOwnProperty(nodeID)) {
  9861. var node = this.nodes[nodeID];
  9862. if (node.inView() == true) {
  9863. if ((node.width*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
  9864. (node.height*this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
  9865. this.openCluster(node);
  9866. }
  9867. }
  9868. }
  9869. }
  9870. };
  9871. /**
  9872. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  9873. * has to be opened based on the current zoom level.
  9874. *
  9875. * @private
  9876. */
  9877. Cluster.prototype._openClusters = function(recursive,force) {
  9878. for (var i = 0; i < this.nodeIndices.length; i++) {
  9879. var node = this.nodes[this.nodeIndices[i]];
  9880. this._expandClusterNode(node,recursive,force);
  9881. }
  9882. };
  9883. /**
  9884. * This function checks if a node has to be opened. This is done by checking the zoom level.
  9885. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  9886. * This recursive behaviour is optional and can be set by the recursive argument.
  9887. *
  9888. * @param {Node} parentNode | to check for cluster and expand
  9889. * @param {Boolean} recursive | enable or disable recursive calling
  9890. * @param {Boolean} force | enable or disable forcing
  9891. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  9892. * @private
  9893. */
  9894. Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) {
  9895. // first check if node is a cluster
  9896. if (parentNode.clusterSize > 1) {
  9897. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  9898. if (parentNode.clusterSize < this.constants.clustering.sectorThreshold) {
  9899. openAll = true;
  9900. }
  9901. recursive = openAll ? true : recursive;
  9902. // if the last child has been added on a smaller scale than current scale (@optimization)
  9903. if (parentNode.formationScale < this.scale || force == true) {
  9904. // we will check if any of the contained child nodes should be removed from the cluster
  9905. for (var containedNodeID in parentNode.containedNodes) {
  9906. if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
  9907. var childNode = parentNode.containedNodes[containedNodeID];
  9908. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  9909. // the largest cluster is the one that comes from outside
  9910. if (force == true) {
  9911. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  9912. || openAll) {
  9913. this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
  9914. }
  9915. }
  9916. else {
  9917. if (this._nodeInActiveArea(parentNode)) {
  9918. this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
  9919. }
  9920. }
  9921. }
  9922. }
  9923. }
  9924. }
  9925. };
  9926. /**
  9927. * ONLY CALLED FROM _expandClusterNode
  9928. *
  9929. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  9930. * the child node from the parent contained_node object and put it back into the global nodes object.
  9931. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  9932. *
  9933. * @param {Node} parentNode | the parent node
  9934. * @param {String} containedNodeID | child_node id as it is contained in the containedNodes object of the parent node
  9935. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  9936. * With force and recursive both true, the entire cluster is unpacked
  9937. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  9938. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  9939. * @private
  9940. */
  9941. Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, force, openAll) {
  9942. var childNode = parentNode.containedNodes[containedNodeID];
  9943. // if child node has been added on smaller scale than current, kick out
  9944. if (childNode.formationScale < this.scale || force == true) {
  9945. // put the child node back in the global nodes object
  9946. this.nodes[containedNodeID] = childNode;
  9947. // release the contained edges from this childNode back into the global edges
  9948. this._releaseContainedEdges(parentNode,childNode);
  9949. // reconnect rerouted edges to the childNode
  9950. this._connectEdgeBackToChild(parentNode,childNode);
  9951. // validate all edges in dynamicEdges
  9952. this._validateEdges(parentNode);
  9953. // undo the changes from the clustering operation on the parent node
  9954. parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
  9955. parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
  9956. parentNode.clusterSize -= childNode.clusterSize;
  9957. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  9958. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  9959. childNode.x = parentNode.x + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
  9960. childNode.y = parentNode.y + this.constants.edges.length * 0.3 * (0.5 - Math.random()) * parentNode.clusterSize;
  9961. // remove node from the list
  9962. delete parentNode.containedNodes[containedNodeID];
  9963. // check if there are other childs with this clusterSession in the parent.
  9964. var othersPresent = false;
  9965. for (var childNodeID in parentNode.containedNodes) {
  9966. if (parentNode.containedNodes.hasOwnProperty(childNodeID)) {
  9967. if (parentNode.containedNodes[childNodeID].clusterSession == childNode.clusterSession) {
  9968. othersPresent = true;
  9969. break;
  9970. }
  9971. }
  9972. }
  9973. // if there are no others, remove the cluster session from the list
  9974. if (othersPresent == false) {
  9975. parentNode.clusterSessions.pop();
  9976. }
  9977. // remove the clusterSession from the child node
  9978. childNode.clusterSession = 0;
  9979. // restart the simulation to reorganise all nodes
  9980. this.moving = true;
  9981. // recalculate the size of the node on the next time the node is rendered
  9982. parentNode.clearSizeCache();
  9983. }
  9984. // check if a further expansion step is possible if recursivity is enabled
  9985. if (recursive == true) {
  9986. this._expandClusterNode(childNode,recursive,force,openAll);
  9987. }
  9988. };
  9989. /**
  9990. * This function checks if any nodes at the end of their trees have edges below a threshold length
  9991. * This function is called only from updateClusters()
  9992. * forceLevelCollapse ignores the length of the edge and collapses one level
  9993. * This means that a node with only one edge will be clustered with its connected node
  9994. *
  9995. * @private
  9996. * @param {Boolean} force
  9997. */
  9998. Cluster.prototype._formClusters = function(force) {
  9999. if (force == false) {
  10000. this._formClustersByZoom();
  10001. }
  10002. else {
  10003. this._forceClustersByZoom();
  10004. }
  10005. };
  10006. /**
  10007. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  10008. *
  10009. * @private
  10010. */
  10011. Cluster.prototype._formClustersByZoom = function() {
  10012. var dx,dy,length,
  10013. minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  10014. // check if any edges are shorter than minLength and start the clustering
  10015. // the clustering favours the node with the larger mass
  10016. for (var edgeID in this.edges) {
  10017. if (this.edges.hasOwnProperty(edgeID)) {
  10018. var edge = this.edges[edgeID];
  10019. if (edge.connected) {
  10020. if (edge.toId != edge.fromId) {
  10021. dx = (edge.to.x - edge.from.x);
  10022. dy = (edge.to.y - edge.from.y);
  10023. length = Math.sqrt(dx * dx + dy * dy);
  10024. if (length < minLength) {
  10025. // first check which node is larger
  10026. var parentNode = edge.from;
  10027. var childNode = edge.to;
  10028. if (edge.to.mass > edge.from.mass) {
  10029. parentNode = edge.to;
  10030. childNode = edge.from;
  10031. }
  10032. if (childNode.dynamicEdgesLength == 1) {
  10033. this._addToCluster(parentNode,childNode,false);
  10034. }
  10035. else if (parentNode.dynamicEdgesLength == 1) {
  10036. this._addToCluster(childNode,parentNode,false);
  10037. }
  10038. }
  10039. }
  10040. }
  10041. }
  10042. }
  10043. };
  10044. /**
  10045. * This function forces the graph to cluster all nodes with only one connecting edge to their
  10046. * connected node.
  10047. *
  10048. * @private
  10049. */
  10050. Cluster.prototype._forceClustersByZoom = function() {
  10051. for (var nodeID in this.nodes) {
  10052. // another node could have absorbed this child.
  10053. if (this.nodes.hasOwnProperty(nodeID)) {
  10054. var childNode = this.nodes[nodeID];
  10055. // the edges can be swallowed by another decrease
  10056. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  10057. var edge = childNode.dynamicEdges[0];
  10058. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  10059. // group to the largest node
  10060. if (childNode.id != parentNode.id) {
  10061. if (parentNode.mass > childNode.mass) {
  10062. this._addToCluster(parentNode,childNode,true);
  10063. }
  10064. else {
  10065. this._addToCluster(childNode,parentNode,true);
  10066. }
  10067. }
  10068. }
  10069. }
  10070. }
  10071. };
  10072. /**
  10073. * This function forms clusters from hubs, it loops over all nodes
  10074. *
  10075. * @param {Boolean} force | Disregard zoom level
  10076. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  10077. * @private
  10078. */
  10079. Cluster.prototype._formClustersByHub = function(force, onlyEqual) {
  10080. // we loop over all nodes in the list
  10081. for (var nodeID in this.nodes) {
  10082. // we check if it is still available since it can be used by the clustering in this loop
  10083. if (this.nodes.hasOwnProperty(nodeID)) {
  10084. this._formClusterFromHub(this.nodes[nodeID],force,onlyEqual);
  10085. }
  10086. }
  10087. };
  10088. /**
  10089. * This function forms a cluster from a specific preselected hub node
  10090. *
  10091. * @param {Node} hubNode | the node we will cluster as a hub
  10092. * @param {Boolean} force | Disregard zoom level
  10093. * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
  10094. * @param {Number} [absorptionSizeOffset] |
  10095. * @private
  10096. */
  10097. Cluster.prototype._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
  10098. if (absorptionSizeOffset === undefined) {
  10099. absorptionSizeOffset = 0;
  10100. }
  10101. // we decide if the node is a hub
  10102. if ((hubNode.dynamicEdgesLength >= this.hubThreshold && onlyEqual == false) ||
  10103. (hubNode.dynamicEdgesLength == this.hubThreshold && onlyEqual == true)) {
  10104. // initialize variables
  10105. var dx,dy,length;
  10106. var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
  10107. var allowCluster = false;
  10108. // we create a list of edges because the dynamicEdges change over the course of this loop
  10109. var edgesIDarray = [];
  10110. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  10111. for (var j = 0; j < amountOfInitialEdges; j++) {
  10112. edgesIDarray.push(hubNode.dynamicEdges[j].id);
  10113. }
  10114. // if the hub clustering is not forces, we check if one of the edges connected
  10115. // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
  10116. if (force == false) {
  10117. allowCluster = false;
  10118. for (j = 0; j < amountOfInitialEdges; j++) {
  10119. var edge = this.edges[edgesIDarray[j]];
  10120. if (edge !== undefined) {
  10121. if (edge.connected) {
  10122. if (edge.toId != edge.fromId) {
  10123. dx = (edge.to.x - edge.from.x);
  10124. dy = (edge.to.y - edge.from.y);
  10125. length = Math.sqrt(dx * dx + dy * dy);
  10126. if (length < minLength) {
  10127. allowCluster = true;
  10128. break;
  10129. }
  10130. }
  10131. }
  10132. }
  10133. }
  10134. }
  10135. // start the clustering if allowed
  10136. if ((!force && allowCluster) || force) {
  10137. // we loop over all edges INITIALLY connected to this hub
  10138. for (j = 0; j < amountOfInitialEdges; j++) {
  10139. edge = this.edges[edgesIDarray[j]];
  10140. // the edge can be clustered by this function in a previous loop
  10141. if (edge !== undefined) {
  10142. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  10143. // we do not want hubs to merge with other hubs nor do we want to cluster itself.
  10144. if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
  10145. (childNode.id != hubNode.id)) {
  10146. this._addToCluster(hubNode,childNode,force);
  10147. }
  10148. }
  10149. }
  10150. }
  10151. }
  10152. };
  10153. /**
  10154. * This function adds the child node to the parent node, creating a cluster if it is not already.
  10155. *
  10156. * @param {Node} parentNode | this is the node that will house the child node
  10157. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  10158. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  10159. * @private
  10160. */
  10161. Cluster.prototype._addToCluster = function(parentNode, childNode, force) {
  10162. // join child node in the parent node
  10163. parentNode.containedNodes[childNode.id] = childNode;
  10164. // manage all the edges connected to the child and parent nodes
  10165. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  10166. var edge = childNode.dynamicEdges[i];
  10167. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  10168. this._addToContainedEdges(parentNode,childNode,edge);
  10169. }
  10170. else {
  10171. this._connectEdgeToCluster(parentNode,childNode,edge);
  10172. }
  10173. }
  10174. // a contained node has no dynamic edges.
  10175. childNode.dynamicEdges = [];
  10176. // remove circular edges from clusters
  10177. this._containCircularEdgesFromNode(parentNode,childNode);
  10178. // remove the childNode from the global nodes object
  10179. delete this.nodes[childNode.id];
  10180. // update the properties of the child and parent
  10181. var massBefore = parentNode.mass;
  10182. childNode.clusterSession = this.clusterSession;
  10183. parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
  10184. parentNode.clusterSize += childNode.clusterSize;
  10185. parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
  10186. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  10187. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  10188. parentNode.clusterSessions.push(this.clusterSession);
  10189. }
  10190. // forced clusters only open from screen size and double tap
  10191. if (force == true) {
  10192. // parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+3);
  10193. parentNode.formationScale = 0;
  10194. }
  10195. else {
  10196. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  10197. }
  10198. // recalculate the size of the node on the next time the node is rendered
  10199. parentNode.clearSizeCache();
  10200. // set the pop-out scale for the childnode
  10201. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  10202. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  10203. childNode.clearVelocity();
  10204. // the mass has altered, preservation of energy dictates the velocity to be updated
  10205. parentNode.updateVelocity(massBefore);
  10206. // restart the simulation to reorganise all nodes
  10207. this.moving = true;
  10208. };
  10209. /**
  10210. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  10211. * This is a seperate function to allow for level-wise collapsing of the node tree.
  10212. * It has to be called if a level is collapsed. It is called by _formClusters().
  10213. * @private
  10214. */
  10215. Cluster.prototype._updateDynamicEdges = function() {
  10216. for (var i = 0; i < this.nodeIndices.length; i++) {
  10217. var node = this.nodes[this.nodeIndices[i]];
  10218. node.dynamicEdgesLength = node.dynamicEdges.length;
  10219. // this corrects for multiple edges pointing at the same other node
  10220. var correction = 0;
  10221. if (node.dynamicEdgesLength > 1) {
  10222. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  10223. var edgeToId = node.dynamicEdges[j].toId;
  10224. var edgeFromId = node.dynamicEdges[j].fromId;
  10225. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  10226. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  10227. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  10228. correction += 1;
  10229. }
  10230. }
  10231. }
  10232. }
  10233. node.dynamicEdgesLength -= correction;
  10234. }
  10235. };
  10236. /**
  10237. * This adds an edge from the childNode to the contained edges of the parent node
  10238. *
  10239. * @param parentNode | Node object
  10240. * @param childNode | Node object
  10241. * @param edge | Edge object
  10242. * @private
  10243. */
  10244. Cluster.prototype._addToContainedEdges = function(parentNode, childNode, edge) {
  10245. // create an array object if it does not yet exist for this childNode
  10246. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  10247. parentNode.containedEdges[childNode.id] = []
  10248. }
  10249. // add this edge to the list
  10250. parentNode.containedEdges[childNode.id].push(edge);
  10251. // remove the edge from the global edges object
  10252. delete this.edges[edge.id];
  10253. // remove the edge from the parent object
  10254. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  10255. if (parentNode.dynamicEdges[i].id == edge.id) {
  10256. parentNode.dynamicEdges.splice(i,1);
  10257. break;
  10258. }
  10259. }
  10260. };
  10261. /**
  10262. * This function connects an edge that was connected to a child node to the parent node.
  10263. * It keeps track of which nodes it has been connected to with the originalID array.
  10264. *
  10265. * @param parentNode | Node object
  10266. * @param childNode | Node object
  10267. * @param edge | Edge object
  10268. * @private
  10269. */
  10270. Cluster.prototype._connectEdgeToCluster = function(parentNode, childNode, edge) {
  10271. // handle circular edges
  10272. if (edge.toId == edge.fromId) {
  10273. this._addToContainedEdges(parentNode, childNode, edge);
  10274. }
  10275. else {
  10276. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  10277. edge.originalToID.push(childNode.id);
  10278. edge.to = parentNode;
  10279. edge.toId = parentNode.id;
  10280. }
  10281. else { // edge connected to other node with the "from" side
  10282. edge.originalFromID.push(childNode.id);
  10283. edge.from = parentNode;
  10284. edge.fromId = parentNode.id;
  10285. }
  10286. this._addToReroutedEdges(parentNode,childNode,edge);
  10287. }
  10288. };
  10289. Cluster.prototype._containCircularEdgesFromNode = function(parentNode, childNode) {
  10290. // manage all the edges connected to the child and parent nodes
  10291. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  10292. var edge = parentNode.dynamicEdges[i];
  10293. // handle circular edges
  10294. if (edge.toId == edge.fromId) {
  10295. this._addToContainedEdges(parentNode, childNode, edge);
  10296. }
  10297. }
  10298. }
  10299. /**
  10300. * This adds an edge from the childNode to the rerouted edges of the parent node
  10301. *
  10302. * @param parentNode | Node object
  10303. * @param childNode | Node object
  10304. * @param edge | Edge object
  10305. * @private
  10306. */
  10307. Cluster.prototype._addToReroutedEdges = function(parentNode, childNode, edge) {
  10308. // create an array object if it does not yet exist for this childNode
  10309. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  10310. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  10311. parentNode.reroutedEdges[childNode.id] = [];
  10312. }
  10313. parentNode.reroutedEdges[childNode.id].push(edge);
  10314. // this edge becomes part of the dynamicEdges of the cluster node
  10315. parentNode.dynamicEdges.push(edge);
  10316. };
  10317. /**
  10318. * This function connects an edge that was connected to a cluster node back to the child node.
  10319. *
  10320. * @param parentNode | Node object
  10321. * @param childNode | Node object
  10322. * @private
  10323. */
  10324. Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
  10325. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  10326. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  10327. var edge = parentNode.reroutedEdges[childNode.id][i];
  10328. if (edge.originalFromID[edge.originalFromID.length-1] == childNode.id) {
  10329. edge.originalFromID.pop();
  10330. edge.fromId = childNode.id;
  10331. edge.from = childNode;
  10332. }
  10333. else {
  10334. edge.originalToID.pop();
  10335. edge.toId = childNode.id;
  10336. edge.to = childNode;
  10337. }
  10338. // append this edge to the list of edges connecting to the childnode
  10339. childNode.dynamicEdges.push(edge);
  10340. // remove the edge from the parent object
  10341. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  10342. if (parentNode.dynamicEdges[j].id == edge.id) {
  10343. parentNode.dynamicEdges.splice(j,1);
  10344. break;
  10345. }
  10346. }
  10347. }
  10348. // remove the entry from the rerouted edges
  10349. delete parentNode.reroutedEdges[childNode.id];
  10350. }
  10351. };
  10352. /**
  10353. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  10354. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  10355. * parentNode
  10356. *
  10357. * @param parentNode | Node object
  10358. * @private
  10359. */
  10360. Cluster.prototype._validateEdges = function(parentNode) {
  10361. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  10362. var edge = parentNode.dynamicEdges[i];
  10363. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  10364. parentNode.dynamicEdges.splice(i,1);
  10365. }
  10366. }
  10367. };
  10368. /**
  10369. * This function released the contained edges back into the global domain and puts them back into the
  10370. * dynamic edges of both parent and child.
  10371. *
  10372. * @param {Node} parentNode |
  10373. * @param {Node} childNode |
  10374. * @private
  10375. */
  10376. Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) {
  10377. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  10378. var edge = parentNode.containedEdges[childNode.id][i];
  10379. // put the edge back in the global edges object
  10380. this.edges[edge.id] = edge;
  10381. // put the edge back in the dynamic edges of the child and parent
  10382. childNode.dynamicEdges.push(edge);
  10383. parentNode.dynamicEdges.push(edge);
  10384. }
  10385. // remove the entry from the contained edges
  10386. delete parentNode.containedEdges[childNode.id];
  10387. };
  10388. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  10389. /**
  10390. * This updates the node labels for all nodes (for debugging purposes)
  10391. */
  10392. Cluster.prototype.updateLabels = function() {
  10393. var nodeID;
  10394. // update node labels
  10395. for (nodeID in this.nodes) {
  10396. if (this.nodes.hasOwnProperty(nodeID)) {
  10397. var node = this.nodes[nodeID];
  10398. if (node.clusterSize > 1) {
  10399. node.label = "[".concat(String(node.clusterSize),"]");
  10400. }
  10401. }
  10402. }
  10403. // update node labels
  10404. for (nodeID in this.nodes) {
  10405. if (this.nodes.hasOwnProperty(nodeID)) {
  10406. node = this.nodes[nodeID];
  10407. if (node.clusterSize == 1) {
  10408. if (node.originalLabel !== undefined) {
  10409. node.label = node.originalLabel;
  10410. }
  10411. else {
  10412. node.label = String(node.id);
  10413. }
  10414. }
  10415. }
  10416. }
  10417. /* Debug Override */
  10418. // for (nodeID in this.nodes) {
  10419. // if (this.nodes.hasOwnProperty(nodeID)) {
  10420. // node = this.nodes[nodeID];
  10421. // node.label = String(Math.round(node.width)).concat(":",Math.round(node.width*this.scale));
  10422. // }
  10423. // }
  10424. };
  10425. /**
  10426. * This function determines if the cluster we want to decluster is in the active area
  10427. * this means around the zoom center
  10428. *
  10429. * @param {Node} node
  10430. * @returns {boolean}
  10431. * @private
  10432. */
  10433. Cluster.prototype._nodeInActiveArea = function(node) {
  10434. return (
  10435. Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  10436. &&
  10437. Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  10438. )
  10439. };
  10440. /**
  10441. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  10442. * It puts large clusters away from the center and randomizes the order.
  10443. *
  10444. */
  10445. Cluster.prototype.repositionNodes = function() {
  10446. for (var i = 0; i < this.nodeIndices.length; i++) {
  10447. var node = this.nodes[this.nodeIndices[i]];
  10448. if (!node.isFixed()) {
  10449. var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize);
  10450. var angle = 2 * Math.PI * Math.random();
  10451. node.x = radius * Math.cos(angle);
  10452. node.y = radius * Math.sin(angle);
  10453. }
  10454. }
  10455. };
  10456. /**
  10457. * We determine how many connections denote an important hub.
  10458. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  10459. *
  10460. * @private
  10461. */
  10462. Cluster.prototype._getHubSize = function() {
  10463. var average = 0;
  10464. var averageSquared = 0;
  10465. var hubCounter = 0;
  10466. var largestHub = 0;
  10467. for (var i = 0; i < this.nodeIndices.length; i++) {
  10468. var node = this.nodes[this.nodeIndices[i]];
  10469. if (node.dynamicEdgesLength > largestHub) {
  10470. largestHub = node.dynamicEdgesLength;
  10471. }
  10472. average += node.dynamicEdgesLength;
  10473. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  10474. hubCounter += 1;
  10475. }
  10476. average = average / hubCounter;
  10477. averageSquared = averageSquared / hubCounter;
  10478. var variance = averageSquared - Math.pow(average,2);
  10479. var standardDeviation = Math.sqrt(variance);
  10480. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  10481. // always have at least one to cluster
  10482. if (this.hubThreshold > largestHub) {
  10483. this.hubThreshold = largestHub;
  10484. }
  10485. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  10486. // console.log("hubThreshold:",this.hubThreshold);
  10487. };
  10488. /**
  10489. * We reduce the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
  10490. * with this amount we can cluster specifically on these snakes.
  10491. *
  10492. * @param {Number} fraction | between 0 and 1, the percentage of snakes to reduce
  10493. * @private
  10494. */
  10495. Cluster.prototype._reduceAmountOfSnakes = function(fraction) {
  10496. this.hubThreshold = 2;
  10497. var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
  10498. for (nodeID in this.nodes) {
  10499. if (this.nodes.hasOwnProperty(nodeID)) {
  10500. if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
  10501. if (reduceAmount > 0) {
  10502. this._formClusterFromHub(this.nodes[nodeID],true,true,1);
  10503. reduceAmount -= 1;
  10504. }
  10505. }
  10506. }
  10507. }
  10508. };
  10509. /**
  10510. * We get the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
  10511. * with this amount we can cluster specifically on these snakes.
  10512. *
  10513. * @private
  10514. */
  10515. Cluster.prototype._getSnakeFraction = function() {
  10516. var snakes = 0;
  10517. var total = 0;
  10518. for (nodeID in this.nodes) {
  10519. if (this.nodes.hasOwnProperty(nodeID)) {
  10520. if (this.nodes[nodeID].dynamicEdgesLength == 2 && this.nodes[nodeID].dynamicEdges.length >= 2) {
  10521. snakes += 1;
  10522. }
  10523. total += 1;
  10524. }
  10525. }
  10526. return snakes/total;
  10527. };
  10528. /**
  10529. * @constructor Graph
  10530. * Create a graph visualization, displaying nodes and edges.
  10531. *
  10532. * @param {Element} container The DOM element in which the Graph will
  10533. * be created. Normally a div element.
  10534. * @param {Object} data An object containing parameters
  10535. * {Array} nodes
  10536. * {Array} edges
  10537. * @param {Object} options Options
  10538. */
  10539. function Graph (container, data, options) {
  10540. // create variables and set default values
  10541. this.containerElement = container;
  10542. this.width = '100%';
  10543. this.height = '100%';
  10544. this.refreshRate = 50; // milliseconds
  10545. this.stabilize = true; // stabilize before displaying the graph
  10546. this.selectable = true;
  10547. // set constant values
  10548. this.constants = {
  10549. nodes: {
  10550. radiusMin: 5,
  10551. radiusMax: 20,
  10552. radius: 5,
  10553. distance: 100, // px
  10554. shape: 'ellipse',
  10555. image: undefined,
  10556. widthMin: 16, // px
  10557. widthMax: 64, // px
  10558. fontColor: 'black',
  10559. fontSize: 14, // px
  10560. //fontFace: verdana,
  10561. fontFace: 'arial',
  10562. color: {
  10563. border: '#2B7CE9',
  10564. background: '#97C2FC',
  10565. highlight: {
  10566. border: '#2B7CE9',
  10567. background: '#D2E5FF'
  10568. }
  10569. },
  10570. borderColor: '#2B7CE9',
  10571. backgroundColor: '#97C2FC',
  10572. highlightColor: '#D2E5FF',
  10573. group: undefined
  10574. },
  10575. edges: {
  10576. widthMin: 1,
  10577. widthMax: 15,
  10578. width: 1,
  10579. style: 'line',
  10580. color: '#343434',
  10581. fontColor: '#343434',
  10582. fontSize: 14, // px
  10583. fontFace: 'arial',
  10584. //distance: 100, //px
  10585. length: 100, // px
  10586. dash: {
  10587. length: 10,
  10588. gap: 5,
  10589. altLength: undefined
  10590. }
  10591. },
  10592. clustering: {
  10593. enableClustering: false, // global on/off switch for clustering.
  10594. maxNumberOfNodes: 100, // for automatic (initial) clustering
  10595. snakeThreshold: 0.7, // maximum percentage of allowed snakenodes (long strings of connected nodes) within all nodes
  10596. clusterEdgeThreshold: 15, // edge length threshold. if smaller, this node is clustered
  10597. sectorThreshold: 50, // cluster size threshold. If larger, expanding in own sector.
  10598. screenSizeThreshold: 0.2, // relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node
  10599. fontSizeMultiplier: 4, // how much the cluster font size grows per node in cluster (in px)
  10600. forceAmplification: 0.6, // factor of increase fo the repulsion force of a cluster (per node in cluster)
  10601. distanceAmplification: 0.2, // factor how much the repulsion distance of a cluster increases (per node in cluster).
  10602. edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
  10603. clusterSizeWidthFactor: 10, // growth of the width per node in cluster
  10604. clusterSizeHeightFactor: 10, // growth of the height per node in cluster
  10605. clusterSizeRadiusFactor: 10, // growth of the radius per node in cluster
  10606. activeAreaBoxSize: 100, // box area around the curser where clusters are popped open
  10607. massTransferCoefficient: 1 // parent.mass += massTransferCoefficient * child.mass
  10608. },
  10609. minForce: 0.05,
  10610. minVelocity: 0.02, // px/s
  10611. maxIterations: 1000 // maximum number of iteration to stabilize
  10612. };
  10613. // call the constructor of the cluster object
  10614. Cluster.call(this);
  10615. // call the sector constructor
  10616. this._loadSectorSystem(); // would be fantastic if multiple in heritance just worked!
  10617. var graph = this;
  10618. this.freezeSimulation = false;// freeze the simulation
  10619. this.tapTimer = 0; // timer to detect doubleclick or double tap
  10620. this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
  10621. this.nodes = {}; // object with Node objects
  10622. this.edges = {}; // object with Edge objects
  10623. this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during calcForces.
  10624. this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during calcForces
  10625. this.zoomCenter = {}; // object with x and y elements used for determining the center of the zoom action
  10626. this.scale = 1; // defining the global scale variable in the constructor
  10627. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  10628. // TODO: create a counter to keep track on the number of nodes having values
  10629. // TODO: create a counter to keep track on the number of nodes currently moving
  10630. // TODO: create a counter to keep track on the number of edges having values
  10631. this.nodesData = null; // A DataSet or DataView
  10632. this.edgesData = null; // A DataSet or DataView
  10633. // create event listeners used to subscribe on the DataSets of the nodes and edges
  10634. var me = this;
  10635. this.nodesListeners = {
  10636. 'add': function (event, params) {
  10637. me._addNodes(params.items);
  10638. me.start();
  10639. },
  10640. 'update': function (event, params) {
  10641. me._updateNodes(params.items);
  10642. me.start();
  10643. },
  10644. 'remove': function (event, params) {
  10645. me._removeNodes(params.items);
  10646. me.start();
  10647. }
  10648. };
  10649. this.edgesListeners = {
  10650. 'add': function (event, params) {
  10651. me._addEdges(params.items);
  10652. me.start();
  10653. },
  10654. 'update': function (event, params) {
  10655. me._updateEdges(params.items);
  10656. me.start();
  10657. },
  10658. 'remove': function (event, params) {
  10659. me._removeEdges(params.items);
  10660. me.start();
  10661. }
  10662. };
  10663. this.groups = new Groups(); // object with groups
  10664. this.images = new Images(); // object with images
  10665. this.images.setOnloadCallback(function () {
  10666. graph._redraw();
  10667. });
  10668. // properties of the data
  10669. this.moving = false; // True if any of the nodes have an undefined position
  10670. this.selection = [];
  10671. this.timer = undefined;
  10672. // create a frame and canvas
  10673. this._create();
  10674. // apply options
  10675. this.setOptions(options);
  10676. // load data (the disable start variable will be the same as the enable clustering)
  10677. this.setData(data,this.constants.clustering.enableClustering); //
  10678. // zoom so all data will fit on the screen
  10679. this.zoomToFit();
  10680. if (this.constants.clustering.enableClustering) {
  10681. // cluster if the data set is big
  10682. this.clusterToFit(this.constants.clustering.maxNumberOfNodes, true);
  10683. // updates the lables after clustering
  10684. this.updateLabels();
  10685. // this is called here because if clusterin is disabled, the start and stabilize are called in
  10686. // the setData function.
  10687. if (this.stabilize) {
  10688. this._doStabilize();
  10689. }
  10690. this.start();
  10691. }
  10692. }
  10693. /**
  10694. * We add the functionality of the cluster object to the graph object
  10695. * @type {Cluster.prototype}
  10696. */
  10697. Graph.prototype = Object.create(Cluster.prototype);
  10698. /**
  10699. * This function zooms out to fit all data on screen based on amount of nodes
  10700. */
  10701. Graph.prototype.zoomToFit = function() {
  10702. var numberOfNodes = this.nodeIndices.length;
  10703. var zoomLevel = 105 / (numberOfNodes + 80); // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  10704. if (zoomLevel > 1.0) {
  10705. zoomLevel = 1.0;
  10706. }
  10707. if (!('mousewheelScale' in this.pinch)) {
  10708. this.pinch.mousewheelScale = zoomLevel;
  10709. }
  10710. this._setScale(zoomLevel);
  10711. };
  10712. /**
  10713. * Update the this.nodeIndices with the most recent node index list
  10714. * @private
  10715. */
  10716. Graph.prototype._updateNodeIndexList = function() {
  10717. this._clearNodeIndexList();
  10718. for (var idx in this.nodes) {
  10719. if (this.nodes.hasOwnProperty(idx)) {
  10720. this.nodeIndices.push(idx);
  10721. }
  10722. }
  10723. };
  10724. /**
  10725. * Set nodes and edges, and optionally options as well.
  10726. *
  10727. * @param {Object} data Object containing parameters:
  10728. * {Array | DataSet | DataView} [nodes] Array with nodes
  10729. * {Array | DataSet | DataView} [edges] Array with edges
  10730. * {String} [dot] String containing data in DOT format
  10731. * {Options} [options] Object with options
  10732. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  10733. */
  10734. Graph.prototype.setData = function(data, disableStart) {
  10735. if (disableStart === undefined) {
  10736. disableStart = false;
  10737. }
  10738. if (data && data.dot && (data.nodes || data.edges)) {
  10739. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  10740. ' parameter pair "nodes" and "edges", but not both.');
  10741. }
  10742. // set options
  10743. this.setOptions(data && data.options);
  10744. // set all data
  10745. if (data && data.dot) {
  10746. // parse DOT file
  10747. if(data && data.dot) {
  10748. var dotData = vis.util.DOTToGraph(data.dot);
  10749. this.setData(dotData);
  10750. return;
  10751. }
  10752. }
  10753. else {
  10754. this._setNodes(data && data.nodes);
  10755. this._setEdges(data && data.edges);
  10756. }
  10757. this._putDataInSector();
  10758. if (!disableStart) {
  10759. // find a stable position or start animating to a stable position
  10760. if (this.stabilize) {
  10761. this._doStabilize();
  10762. }
  10763. this.start();
  10764. }
  10765. };
  10766. /**
  10767. * Set options
  10768. * @param {Object} options
  10769. */
  10770. Graph.prototype.setOptions = function (options) {
  10771. if (options) {
  10772. // retrieve parameter values
  10773. if (options.width !== undefined) {this.width = options.width;}
  10774. if (options.height !== undefined) {this.height = options.height;}
  10775. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  10776. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  10777. if (options.clustering) {
  10778. for (var prop in optiones.clustering) {
  10779. if (options.clustering.hasOwnProperty(prop)) {
  10780. this.constants.clustering[prop] = options.clustering[prop];
  10781. }
  10782. }
  10783. }
  10784. // TODO: work out these options and document them
  10785. if (options.edges) {
  10786. for (prop in options.edges) {
  10787. if (options.edges.hasOwnProperty(prop)) {
  10788. this.constants.edges[prop] = options.edges[prop];
  10789. }
  10790. }
  10791. if (options.edges.length !== undefined &&
  10792. options.nodes && options.nodes.distance === undefined) {
  10793. this.constants.edges.length = options.edges.length;
  10794. this.constants.nodes.distance = options.edges.length * 1.25;
  10795. }
  10796. if (!options.edges.fontColor) {
  10797. this.constants.edges.fontColor = options.edges.color;
  10798. }
  10799. // Added to support dashed lines
  10800. // David Jordan
  10801. // 2012-08-08
  10802. if (options.edges.dash) {
  10803. if (options.edges.dash.length !== undefined) {
  10804. this.constants.edges.dash.length = options.edges.dash.length;
  10805. }
  10806. if (options.edges.dash.gap !== undefined) {
  10807. this.constants.edges.dash.gap = options.edges.dash.gap;
  10808. }
  10809. if (options.edges.dash.altLength !== undefined) {
  10810. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  10811. }
  10812. }
  10813. }
  10814. if (options.nodes) {
  10815. for (prop in options.nodes) {
  10816. if (options.nodes.hasOwnProperty(prop)) {
  10817. this.constants.nodes[prop] = options.nodes[prop];
  10818. }
  10819. }
  10820. if (options.nodes.color) {
  10821. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  10822. }
  10823. /*
  10824. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  10825. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  10826. */
  10827. }
  10828. if (options.groups) {
  10829. for (var groupname in options.groups) {
  10830. if (options.groups.hasOwnProperty(groupname)) {
  10831. var group = options.groups[groupname];
  10832. this.groups.add(groupname, group);
  10833. }
  10834. }
  10835. }
  10836. }
  10837. this.setSize(this.width, this.height);
  10838. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  10839. this._setScale(1);
  10840. };
  10841. /**
  10842. * fire an event
  10843. * @param {String} event The name of an event, for example 'select'
  10844. * @param {Object} params Optional object with event parameters
  10845. * @private
  10846. */
  10847. Graph.prototype._trigger = function (event, params) {
  10848. events.trigger(this, event, params);
  10849. };
  10850. /**
  10851. * Create the main frame for the Graph.
  10852. * This function is executed once when a Graph object is created. The frame
  10853. * contains a canvas, and this canvas contains all objects like the axis and
  10854. * nodes.
  10855. * @private
  10856. */
  10857. Graph.prototype._create = function () {
  10858. // remove all elements from the container element.
  10859. while (this.containerElement.hasChildNodes()) {
  10860. this.containerElement.removeChild(this.containerElement.firstChild);
  10861. }
  10862. this.frame = document.createElement('div');
  10863. this.frame.className = 'graph-frame';
  10864. this.frame.style.position = 'relative';
  10865. this.frame.style.overflow = 'hidden';
  10866. // create the graph canvas (HTML canvas element)
  10867. this.frame.canvas = document.createElement( 'canvas' );
  10868. this.frame.canvas.style.position = 'relative';
  10869. this.frame.appendChild(this.frame.canvas);
  10870. if (!this.frame.canvas.getContext) {
  10871. var noCanvas = document.createElement( 'DIV' );
  10872. noCanvas.style.color = 'red';
  10873. noCanvas.style.fontWeight = 'bold' ;
  10874. noCanvas.style.padding = '10px';
  10875. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  10876. this.frame.canvas.appendChild(noCanvas);
  10877. }
  10878. var me = this;
  10879. this.drag = {};
  10880. this.pinch = {};
  10881. this.hammer = Hammer(this.frame.canvas, {
  10882. prevent_default: true
  10883. });
  10884. this.hammer.on('tap', me._onTap.bind(me) );
  10885. this.hammer.on('hold', me._onHold.bind(me) );
  10886. this.hammer.on('pinch', me._onPinch.bind(me) );
  10887. this.hammer.on('touch', me._onTouch.bind(me) );
  10888. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  10889. this.hammer.on('drag', me._onDrag.bind(me) );
  10890. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  10891. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  10892. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  10893. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  10894. this.mouseTrap = mouseTrap;
  10895. this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me));
  10896. this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
  10897. this.mouseTrap.bind("s",this.singleStep.bind(me));
  10898. this.mouseTrap.bind("h",this.updateClustersDefault.bind(me));
  10899. this.mouseTrap.bind("c",this._collapseSector.bind(me));
  10900. this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
  10901. // add the frame to the container element
  10902. this.containerElement.appendChild(this.frame);
  10903. };
  10904. /**
  10905. *
  10906. * @param {{x: Number, y: Number}} pointer
  10907. * @return {Number | null} node
  10908. * @private
  10909. */
  10910. Graph.prototype._getNodeAt = function (pointer) {
  10911. var x = this._canvasToX(pointer.x);
  10912. var y = this._canvasToY(pointer.y);
  10913. var obj = {
  10914. left: x,
  10915. top: y,
  10916. right: x,
  10917. bottom: y
  10918. };
  10919. // if there are overlapping nodes, select the last one, this is the
  10920. // one which is drawn on top of the others
  10921. var overlappingNodes = this._getNodesOverlappingWith(obj);
  10922. return (overlappingNodes.length > 0) ?
  10923. overlappingNodes[overlappingNodes.length - 1] : null;
  10924. };
  10925. /**
  10926. * Get the pointer location from a touch location
  10927. * @param {{pageX: Number, pageY: Number}} touch
  10928. * @return {{x: Number, y: Number}} pointer
  10929. * @private
  10930. */
  10931. Graph.prototype._getPointer = function (touch) {
  10932. return {
  10933. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  10934. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  10935. };
  10936. };
  10937. /**
  10938. * On start of a touch gesture, store the pointer
  10939. * @param event
  10940. * @private
  10941. */
  10942. Graph.prototype._onTouch = function (event) {
  10943. this.drag.pointer = this._getPointer(event.gesture.touches[0]);
  10944. this.drag.pinched = false;
  10945. this.pinch.scale = this._getScale();
  10946. };
  10947. /**
  10948. * handle drag start event
  10949. * @private
  10950. */
  10951. Graph.prototype._onDragStart = function () {
  10952. var drag = this.drag;
  10953. drag.selection = [];
  10954. drag.translation = this._getTranslation();
  10955. drag.nodeId = this._getNodeAt(drag.pointer);
  10956. // note: drag.pointer is set in _onTouch to get the initial touch location
  10957. var node = this.nodes[drag.nodeId];
  10958. if (node) {
  10959. // select the clicked node if not yet selected
  10960. if (!node.isSelected()) {
  10961. this._selectNodes([drag.nodeId]);
  10962. }
  10963. // create an array with the selected nodes and their original location and status
  10964. var me = this;
  10965. this.selection.forEach(function (id) {
  10966. var node = me.nodes[id];
  10967. if (node) {
  10968. var s = {
  10969. id: id,
  10970. node: node,
  10971. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  10972. x: node.x,
  10973. y: node.y,
  10974. xFixed: node.xFixed,
  10975. yFixed: node.yFixed
  10976. };
  10977. node.xFixed = true;
  10978. node.yFixed = true;
  10979. drag.selection.push(s);
  10980. }
  10981. });
  10982. }
  10983. };
  10984. /**
  10985. * handle drag event
  10986. * @private
  10987. */
  10988. Graph.prototype._onDrag = function (event) {
  10989. if (this.drag.pinched) {
  10990. return;
  10991. }
  10992. var pointer = this._getPointer(event.gesture.touches[0]);
  10993. var me = this,
  10994. drag = this.drag,
  10995. selection = drag.selection;
  10996. if (selection && selection.length) {
  10997. // calculate delta's and new location
  10998. var deltaX = pointer.x - drag.pointer.x,
  10999. deltaY = pointer.y - drag.pointer.y;
  11000. // update position of all selected nodes
  11001. selection.forEach(function (s) {
  11002. var node = s.node;
  11003. if (!s.xFixed) {
  11004. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  11005. }
  11006. if (!s.yFixed) {
  11007. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  11008. }
  11009. });
  11010. // start animation if not yet running
  11011. if (!this.moving) {
  11012. this.moving = true;
  11013. this.start();
  11014. }
  11015. }
  11016. else {
  11017. // move the graph
  11018. var diffX = pointer.x - this.drag.pointer.x;
  11019. var diffY = pointer.y - this.drag.pointer.y;
  11020. this._setTranslation(
  11021. this.drag.translation.x + diffX,
  11022. this.drag.translation.y + diffY);
  11023. this._redraw();
  11024. this.moved = true;
  11025. }
  11026. };
  11027. /**
  11028. * handle drag start event
  11029. * @private
  11030. */
  11031. Graph.prototype._onDragEnd = function () {
  11032. var selection = this.drag.selection;
  11033. if (selection) {
  11034. selection.forEach(function (s) {
  11035. // restore original xFixed and yFixed
  11036. s.node.xFixed = s.xFixed;
  11037. s.node.yFixed = s.yFixed;
  11038. });
  11039. }
  11040. };
  11041. /**
  11042. * handle tap/click event: select/unselect a node
  11043. * @private
  11044. */
  11045. Graph.prototype._onTap = function (event) {
  11046. var pointer = this._getPointer(event.gesture.touches[0]);
  11047. var nodeId = this._getNodeAt(pointer);
  11048. var node = this.nodes[nodeId];
  11049. var elapsedTime = new Date().getTime() - this.tapTimer;
  11050. this.tapTimer = new Date().getTime();
  11051. if (node) {
  11052. if (node.isSelected() && elapsedTime < 300) {
  11053. this.openCluster(node);
  11054. }
  11055. // select this node
  11056. this._selectNodes([nodeId]);
  11057. if (!this.moving) {
  11058. this._redraw();
  11059. }
  11060. }
  11061. else {
  11062. // remove selection
  11063. this._unselectNodes();
  11064. this._redraw();
  11065. }
  11066. };
  11067. /**
  11068. * handle long tap event: multi select nodes
  11069. * @private
  11070. */
  11071. Graph.prototype._onHold = function (event) {
  11072. var pointer = this._getPointer(event.gesture.touches[0]);
  11073. var nodeId = this._getNodeAt(pointer);
  11074. var node = this.nodes[nodeId];
  11075. if (node) {
  11076. if (!node.isSelected()) {
  11077. // select this node, keep previous selection
  11078. var append = true;
  11079. this._selectNodes([nodeId], append);
  11080. }
  11081. else {
  11082. this._unselectNodes([nodeId]);
  11083. }
  11084. if (!this.moving) {
  11085. this._redraw();
  11086. }
  11087. }
  11088. else {
  11089. // Do nothing
  11090. }
  11091. };
  11092. /**
  11093. * Handle pinch event
  11094. * @param event
  11095. * @private
  11096. */
  11097. Graph.prototype._onPinch = function (event) {
  11098. var pointer = this._getPointer(event.gesture.center);
  11099. this.drag.pinched = true;
  11100. if (!('scale' in this.pinch)) {
  11101. this.pinch.scale = 1;
  11102. }
  11103. // TODO: enable moving while pinching?
  11104. var scale = this.pinch.scale * event.gesture.scale;
  11105. this._zoom(scale, pointer)
  11106. };
  11107. /**
  11108. * Zoom the graph in or out
  11109. * @param {Number} scale a number around 1, and between 0.01 and 10
  11110. * @param {{x: Number, y: Number}} pointer
  11111. * @return {Number} appliedScale scale is limited within the boundaries
  11112. * @private
  11113. */
  11114. Graph.prototype._zoom = function(scale, pointer) {
  11115. var scaleOld = this._getScale();
  11116. if (scale < 0.001) {
  11117. scale = 0.001;
  11118. }
  11119. if (scale > 10) {
  11120. scale = 10;
  11121. }
  11122. // + this.frame.canvas.clientHeight / 2
  11123. var translation = this._getTranslation();
  11124. var scaleFrac = scale / scaleOld;
  11125. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  11126. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  11127. this.zoomCenter = {"x" : this._canvasToX(pointer.x),
  11128. "y" : this._canvasToY(pointer.y)};
  11129. // this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
  11130. this._setScale(scale);
  11131. this._setTranslation(tx, ty);
  11132. this.updateClustersDefault();
  11133. this._redraw();
  11134. // console.log("current zoomscale:",this.scale);
  11135. return scale;
  11136. };
  11137. /**
  11138. * Event handler for mouse wheel event, used to zoom the timeline
  11139. * See http://adomas.org/javascript-mouse-wheel/
  11140. * https://github.com/EightMedia/hammer.js/issues/256
  11141. * @param {MouseEvent} event
  11142. * @private
  11143. */
  11144. Graph.prototype._onMouseWheel = function(event) {
  11145. // retrieve delta
  11146. var delta = 0;
  11147. if (event.wheelDelta) { /* IE/Opera. */
  11148. delta = event.wheelDelta/120;
  11149. } else if (event.detail) { /* Mozilla case. */
  11150. // In Mozilla, sign of delta is different than in IE.
  11151. // Also, delta is multiple of 3.
  11152. delta = -event.detail/3;
  11153. }
  11154. // If delta is nonzero, handle it.
  11155. // Basically, delta is now positive if wheel was scrolled up,
  11156. // and negative, if wheel was scrolled down.
  11157. if (delta) {
  11158. if (!('mousewheelScale' in this.pinch)) {
  11159. this.pinch.mousewheelScale = 1;
  11160. }
  11161. // calculate the new scale
  11162. var scale = this.pinch.mousewheelScale;
  11163. var zoom = delta / 10;
  11164. if (delta < 0) {
  11165. zoom = zoom / (1 - zoom);
  11166. }
  11167. scale *= (1 + zoom);
  11168. // calculate the pointer location
  11169. var gesture = util.fakeGesture(this, event);
  11170. var pointer = this._getPointer(gesture.center);
  11171. // apply the new scale
  11172. scale = this._zoom(scale, pointer);
  11173. // store the new, applied scale
  11174. this.pinch.mousewheelScale = scale;
  11175. }
  11176. // Prevent default actions caused by mouse wheel.
  11177. event.preventDefault();
  11178. };
  11179. /**
  11180. * Mouse move handler for checking whether the title moves over a node with a title.
  11181. * @param {Event} event
  11182. * @private
  11183. */
  11184. Graph.prototype._onMouseMoveTitle = function (event) {
  11185. var gesture = util.fakeGesture(this, event);
  11186. var pointer = this._getPointer(gesture.center);
  11187. // check if the previously selected node is still selected
  11188. if (this.popupNode) {
  11189. this._checkHidePopup(pointer);
  11190. }
  11191. // start a timeout that will check if the mouse is positioned above
  11192. // an element
  11193. var me = this;
  11194. var checkShow = function() {
  11195. me._checkShowPopup(pointer);
  11196. };
  11197. if (this.popupTimer) {
  11198. clearInterval(this.popupTimer); // stop any running timer
  11199. }
  11200. if (!this.leftButtonDown) {
  11201. this.popupTimer = setTimeout(checkShow, 300);
  11202. }
  11203. };
  11204. /**
  11205. * Check if there is an element on the given position in the graph
  11206. * (a node or edge). If so, and if this element has a title,
  11207. * show a popup window with its title.
  11208. *
  11209. * @param {{x:Number, y:Number}} pointer
  11210. * @private
  11211. */
  11212. Graph.prototype._checkShowPopup = function (pointer) {
  11213. var obj = {
  11214. left: this._canvasToX(pointer.x),
  11215. top: this._canvasToY(pointer.y),
  11216. right: this._canvasToX(pointer.x),
  11217. bottom: this._canvasToY(pointer.y)
  11218. };
  11219. var id;
  11220. var lastPopupNode = this.popupNode;
  11221. if (this.popupNode == undefined) {
  11222. // search the nodes for overlap, select the top one in case of multiple nodes
  11223. var nodes = this.nodes;
  11224. for (id in nodes) {
  11225. if (nodes.hasOwnProperty(id)) {
  11226. var node = nodes[id];
  11227. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  11228. this.popupNode = node;
  11229. break;
  11230. }
  11231. }
  11232. }
  11233. }
  11234. if (this.popupNode == undefined) {
  11235. // search the edges for overlap
  11236. var edges = this.edges;
  11237. for (id in edges) {
  11238. if (edges.hasOwnProperty(id)) {
  11239. var edge = edges[id];
  11240. if (edge.connected && (edge.getTitle() != undefined) &&
  11241. edge.isOverlappingWith(obj)) {
  11242. this.popupNode = edge;
  11243. break;
  11244. }
  11245. }
  11246. }
  11247. }
  11248. if (this.popupNode) {
  11249. // show popup message window
  11250. if (this.popupNode != lastPopupNode) {
  11251. var me = this;
  11252. if (!me.popup) {
  11253. me.popup = new Popup(me.frame);
  11254. }
  11255. // adjust a small offset such that the mouse cursor is located in the
  11256. // bottom left location of the popup, and you can easily move over the
  11257. // popup area
  11258. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  11259. me.popup.setText(me.popupNode.getTitle());
  11260. me.popup.show();
  11261. }
  11262. }
  11263. else {
  11264. if (this.popup) {
  11265. this.popup.hide();
  11266. }
  11267. }
  11268. };
  11269. /**
  11270. * Check if the popup must be hided, which is the case when the mouse is no
  11271. * longer hovering on the object
  11272. * @param {{x:Number, y:Number}} pointer
  11273. * @private
  11274. */
  11275. Graph.prototype._checkHidePopup = function (pointer) {
  11276. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  11277. this.popupNode = undefined;
  11278. if (this.popup) {
  11279. this.popup.hide();
  11280. }
  11281. }
  11282. };
  11283. /**
  11284. * Unselect selected nodes. If no selection array is provided, all nodes
  11285. * are unselected
  11286. * @param {Object[]} selection Array with selection objects, each selection
  11287. * object has a parameter row. Optional
  11288. * @param {Boolean} triggerSelect If true (default), the select event
  11289. * is triggered when nodes are unselected
  11290. * @return {Boolean} changed True if the selection is changed
  11291. * @private
  11292. */
  11293. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  11294. var changed = false;
  11295. var i, iMax, id;
  11296. if (selection) {
  11297. // remove provided selections
  11298. for (i = 0, iMax = selection.length; i < iMax; i++) {
  11299. id = selection[i];
  11300. if (this.nodes.hasOwnProperty(id)) {
  11301. this.nodes[id].unselect();
  11302. }
  11303. var j = 0;
  11304. while (j < this.selection.length) {
  11305. if (this.selection[j] == id) {
  11306. this.selection.splice(j, 1);
  11307. changed = true;
  11308. }
  11309. else {
  11310. j++;
  11311. }
  11312. }
  11313. }
  11314. }
  11315. else if (this.selection && this.selection.length) {
  11316. // remove all selections
  11317. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  11318. id = this.selection[i];
  11319. if (this.nodes.hasOwnProperty(id)) {
  11320. this.nodes[id].unselect();
  11321. }
  11322. changed = true;
  11323. }
  11324. this.selection = [];
  11325. }
  11326. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  11327. // fire the select event
  11328. this._trigger('select');
  11329. }
  11330. return changed;
  11331. };
  11332. /**
  11333. * select all nodes on given location x, y
  11334. * @param {Array} selection an array with node ids
  11335. * @param {boolean} append If true, the new selection will be appended to the
  11336. * current selection (except for duplicate entries)
  11337. * @return {Boolean} changed True if the selection is changed
  11338. * @private
  11339. */
  11340. Graph.prototype._selectNodes = function(selection, append) {
  11341. var changed = false;
  11342. var i, iMax;
  11343. // TODO: the selectNodes method is a little messy, rework this
  11344. // check if the current selection equals the desired selection
  11345. var selectionAlreadyThere = true;
  11346. if (selection.length != this.selection.length) {
  11347. selectionAlreadyThere = false;
  11348. }
  11349. else {
  11350. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  11351. if (selection[i] != this.selection[i]) {
  11352. selectionAlreadyThere = false;
  11353. break;
  11354. }
  11355. }
  11356. }
  11357. if (selectionAlreadyThere) {
  11358. return changed;
  11359. }
  11360. if (append == undefined || append == false) {
  11361. // first deselect any selected node
  11362. var triggerSelect = false;
  11363. changed = this._unselectNodes(undefined, triggerSelect);
  11364. }
  11365. for (i = 0, iMax = selection.length; i < iMax; i++) {
  11366. // add each of the new selections, but only when they are not duplicate
  11367. var id = selection[i];
  11368. var isDuplicate = (this.selection.indexOf(id) != -1);
  11369. if (!isDuplicate) {
  11370. this.nodes[id].select();
  11371. this.selection.push(id);
  11372. changed = true;
  11373. }
  11374. }
  11375. if (changed) {
  11376. // fire the select event
  11377. this._trigger('select');
  11378. }
  11379. return changed;
  11380. };
  11381. /**
  11382. * retrieve all nodes overlapping with given object
  11383. * @param {Object} obj An object with parameters left, top, right, bottom
  11384. * @return {Number[]} An array with id's of the overlapping nodes
  11385. * @private
  11386. */
  11387. Graph.prototype._getNodesOverlappingWith = function (obj) {
  11388. var overlappingNodes = [];
  11389. var nodes, sector;
  11390. // search in all sectors for nodes
  11391. for (sector in this.sectors["active"]) {
  11392. if (this.sectors["active"].hasOwnProperty(sector)) {
  11393. nodes = this.sectors["active"][sector]["nodes"];
  11394. for (var id in nodes) {
  11395. if (nodes.hasOwnProperty(id)) {
  11396. if (nodes[id].isOverlappingWith(obj)) {
  11397. overlappingNodes.push(id);
  11398. }
  11399. }
  11400. }
  11401. }
  11402. }
  11403. for (sector in this.sectors["frozen"]) {
  11404. if (this.sectors["frozen"].hasOwnProperty(sector)) {
  11405. nodes = this.sectors["frozen"][sector]["nodes"];
  11406. for (var id in nodes) {
  11407. if (nodes.hasOwnProperty(id)) {
  11408. if (nodes[id].isOverlappingWith(obj)) {
  11409. overlappingNodes.push(id);
  11410. }
  11411. }
  11412. }
  11413. }
  11414. }
  11415. this.nodes = this.sectors["active"][this.activeSector[this.activeSector.length-1]]["nodes"];
  11416. return overlappingNodes;
  11417. };
  11418. /**
  11419. * retrieve the currently selected nodes
  11420. * @return {Number[] | String[]} selection An array with the ids of the
  11421. * selected nodes.
  11422. */
  11423. Graph.prototype.getSelection = function() {
  11424. return this.selection.concat([]);
  11425. };
  11426. /**
  11427. * select zero or more nodes
  11428. * @param {Number[] | String[]} selection An array with the ids of the
  11429. * selected nodes.
  11430. */
  11431. Graph.prototype.setSelection = function(selection) {
  11432. var i, iMax, id;
  11433. if (!selection || (selection.length == undefined))
  11434. throw 'Selection must be an array with ids';
  11435. // first unselect any selected node
  11436. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  11437. id = this.selection[i];
  11438. this.nodes[id].unselect();
  11439. }
  11440. this.selection = [];
  11441. for (i = 0, iMax = selection.length; i < iMax; i++) {
  11442. id = selection[i];
  11443. var node = this.nodes[id];
  11444. if (!node) {
  11445. throw new RangeError('Node with id "' + id + '" not found');
  11446. }
  11447. node.select();
  11448. this.selection.push(id);
  11449. }
  11450. this.redraw();
  11451. };
  11452. /**
  11453. * Validate the selection: remove ids of nodes which no longer exist
  11454. * @private
  11455. */
  11456. Graph.prototype._updateSelection = function () {
  11457. var i = 0;
  11458. while (i < this.selection.length) {
  11459. var id = this.selection[i];
  11460. if (!this.nodes[id]) {
  11461. this.selection.splice(i, 1);
  11462. }
  11463. else {
  11464. i++;
  11465. }
  11466. }
  11467. };
  11468. /**
  11469. * Temporary method to test calculating a hub value for the nodes
  11470. * @param {number} level Maximum number edges between two nodes in order
  11471. * to call them connected. Optional, 1 by default
  11472. * @return {Number[]} connectioncount array with the connection count
  11473. * for each node
  11474. * @private
  11475. */
  11476. Graph.prototype._getConnectionCount = function(level) {
  11477. if (level == undefined) {
  11478. level = 1;
  11479. }
  11480. // get the nodes connected to given nodes
  11481. function getConnectedNodes(nodes) {
  11482. var connectedNodes = [];
  11483. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  11484. var node = nodes[j];
  11485. // find all nodes connected to this node
  11486. var edges = node.edges;
  11487. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  11488. var edge = edges[i];
  11489. var other = null;
  11490. // check if connected
  11491. if (edge.from == node)
  11492. other = edge.to;
  11493. else if (edge.to == node)
  11494. other = edge.from;
  11495. // check if the other node is not already in the list with nodes
  11496. var k, kMax;
  11497. if (other) {
  11498. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  11499. if (nodes[k] == other) {
  11500. other = null;
  11501. break;
  11502. }
  11503. }
  11504. }
  11505. if (other) {
  11506. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  11507. if (connectedNodes[k] == other) {
  11508. other = null;
  11509. break;
  11510. }
  11511. }
  11512. }
  11513. if (other)
  11514. connectedNodes.push(other);
  11515. }
  11516. }
  11517. return connectedNodes;
  11518. }
  11519. var connections = [];
  11520. var nodes = this.nodes;
  11521. for (var id in nodes) {
  11522. if (nodes.hasOwnProperty(id)) {
  11523. var c = [nodes[id]];
  11524. for (var l = 0; l < level; l++) {
  11525. c = c.concat(getConnectedNodes(c));
  11526. }
  11527. connections.push(c);
  11528. }
  11529. }
  11530. var hubs = [];
  11531. for (var i = 0, len = connections.length; i < len; i++) {
  11532. hubs.push(connections[i].length);
  11533. }
  11534. return hubs;
  11535. };
  11536. /**
  11537. * Set a new size for the graph
  11538. * @param {string} width Width in pixels or percentage (for example '800px'
  11539. * or '50%')
  11540. * @param {string} height Height in pixels or percentage (for example '400px'
  11541. * or '30%')
  11542. */
  11543. Graph.prototype.setSize = function(width, height) {
  11544. this.frame.style.width = width;
  11545. this.frame.style.height = height;
  11546. this.frame.canvas.style.width = '100%';
  11547. this.frame.canvas.style.height = '100%';
  11548. this.frame.canvas.width = this.frame.canvas.clientWidth;
  11549. this.frame.canvas.height = this.frame.canvas.clientHeight;
  11550. };
  11551. /**
  11552. * Set a data set with nodes for the graph
  11553. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  11554. * @private
  11555. */
  11556. Graph.prototype._setNodes = function(nodes) {
  11557. var oldNodesData = this.nodesData;
  11558. if (nodes instanceof DataSet || nodes instanceof DataView) {
  11559. this.nodesData = nodes;
  11560. }
  11561. else if (nodes instanceof Array) {
  11562. this.nodesData = new DataSet();
  11563. this.nodesData.add(nodes);
  11564. }
  11565. else if (!nodes) {
  11566. this.nodesData = new DataSet();
  11567. }
  11568. else {
  11569. throw new TypeError('Array or DataSet expected');
  11570. }
  11571. if (oldNodesData) {
  11572. // unsubscribe from old dataset
  11573. util.forEach(this.nodesListeners, function (callback, event) {
  11574. oldNodesData.unsubscribe(event, callback);
  11575. });
  11576. }
  11577. // remove drawn nodes
  11578. this.nodes = {};
  11579. if (this.nodesData) {
  11580. // subscribe to new dataset
  11581. var me = this;
  11582. util.forEach(this.nodesListeners, function (callback, event) {
  11583. me.nodesData.subscribe(event, callback);
  11584. });
  11585. // draw all new nodes
  11586. var ids = this.nodesData.getIds();
  11587. this._addNodes(ids);
  11588. }
  11589. this._updateSelection();
  11590. };
  11591. /**
  11592. * Add nodes
  11593. * @param {Number[] | String[]} ids
  11594. * @private
  11595. */
  11596. Graph.prototype._addNodes = function(ids) {
  11597. var id;
  11598. for (var i = 0, len = ids.length; i < len; i++) {
  11599. id = ids[i];
  11600. var data = this.nodesData.get(id);
  11601. var node = new Node(data, this.images, this.groups, this.constants);
  11602. this.nodes[id] = node; // note: this may replace an existing node
  11603. if (!node.isFixed()) {
  11604. // TODO: position new nodes in a smarter way!
  11605. var radius = this.constants.edges.length * 2;
  11606. var count = ids.length;
  11607. var angle = 2 * Math.PI * (i / count);
  11608. node.x = radius * Math.cos(angle);
  11609. node.y = radius * Math.sin(angle);
  11610. // note: no not use node.isMoving() here, as that gives the current
  11611. // velocity of the node, which is zero after creation of the node.
  11612. this.moving = true;
  11613. }
  11614. }
  11615. this._updateNodeIndexList();
  11616. this._reconnectEdges();
  11617. this._updateValueRange(this.nodes);
  11618. };
  11619. /**
  11620. * Update existing nodes, or create them when not yet existing
  11621. * @param {Number[] | String[]} ids
  11622. * @private
  11623. */
  11624. Graph.prototype._updateNodes = function(ids) {
  11625. var nodes = this.nodes,
  11626. nodesData = this.nodesData;
  11627. for (var i = 0, len = ids.length; i < len; i++) {
  11628. var id = ids[i];
  11629. var node = nodes[id];
  11630. var data = nodesData.get(id);
  11631. if (node) {
  11632. // update node
  11633. node.setProperties(data, this.constants);
  11634. }
  11635. else {
  11636. // create node
  11637. node = new Node(properties, this.images, this.groups, this.constants);
  11638. nodes[id] = node;
  11639. if (!node.isFixed()) {
  11640. this.moving = true;
  11641. }
  11642. }
  11643. }
  11644. this._updateNodeIndexList();
  11645. this._reconnectEdges();
  11646. this._updateValueRange(nodes);
  11647. };
  11648. /**
  11649. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  11650. * @param {Number[] | String[]} ids
  11651. * @private
  11652. */
  11653. Graph.prototype._removeNodes = function(ids) {
  11654. var nodes = this.nodes;
  11655. for (var i = 0, len = ids.length; i < len; i++) {
  11656. var id = ids[i];
  11657. delete nodes[id];
  11658. }
  11659. this._updateNodeIndexList();
  11660. this._reconnectEdges();
  11661. this._updateSelection();
  11662. this._updateValueRange(nodes);
  11663. };
  11664. /**
  11665. * Load edges by reading the data table
  11666. * @param {Array | DataSet | DataView} edges The data containing the edges.
  11667. * @private
  11668. * @private
  11669. */
  11670. Graph.prototype._setEdges = function(edges) {
  11671. var oldEdgesData = this.edgesData;
  11672. if (edges instanceof DataSet || edges instanceof DataView) {
  11673. this.edgesData = edges;
  11674. }
  11675. else if (edges instanceof Array) {
  11676. this.edgesData = new DataSet();
  11677. this.edgesData.add(edges);
  11678. }
  11679. else if (!edges) {
  11680. this.edgesData = new DataSet();
  11681. }
  11682. else {
  11683. throw new TypeError('Array or DataSet expected');
  11684. }
  11685. if (oldEdgesData) {
  11686. // unsubscribe from old dataset
  11687. util.forEach(this.edgesListeners, function (callback, event) {
  11688. oldEdgesData.unsubscribe(event, callback);
  11689. });
  11690. }
  11691. // remove drawn edges
  11692. this.edges = {};
  11693. if (this.edgesData) {
  11694. // subscribe to new dataset
  11695. var me = this;
  11696. util.forEach(this.edgesListeners, function (callback, event) {
  11697. me.edgesData.subscribe(event, callback);
  11698. });
  11699. // draw all new nodes
  11700. var ids = this.edgesData.getIds();
  11701. this._addEdges(ids);
  11702. }
  11703. this._reconnectEdges();
  11704. };
  11705. /**
  11706. * Add edges
  11707. * @param {Number[] | String[]} ids
  11708. * @private
  11709. */
  11710. Graph.prototype._addEdges = function (ids) {
  11711. var edges = this.edges,
  11712. edgesData = this.edgesData;
  11713. for (var i = 0, len = ids.length; i < len; i++) {
  11714. var id = ids[i];
  11715. var oldEdge = edges[id];
  11716. if (oldEdge) {
  11717. oldEdge.disconnect();
  11718. }
  11719. var data = edgesData.get(id, {"showInternalIds" : true});
  11720. edges[id] = new Edge(data, this, this.constants);
  11721. }
  11722. this.moving = true;
  11723. this._updateValueRange(edges);
  11724. };
  11725. /**
  11726. * Update existing edges, or create them when not yet existing
  11727. * @param {Number[] | String[]} ids
  11728. * @private
  11729. */
  11730. Graph.prototype._updateEdges = function (ids) {
  11731. var edges = this.edges,
  11732. edgesData = this.edgesData;
  11733. for (var i = 0, len = ids.length; i < len; i++) {
  11734. var id = ids[i];
  11735. var data = edgesData.get(id);
  11736. var edge = edges[id];
  11737. if (edge) {
  11738. // update edge
  11739. edge.disconnect();
  11740. edge.setProperties(data, this.constants);
  11741. edge.connect();
  11742. }
  11743. else {
  11744. // create edge
  11745. edge = new Edge(data, this, this.constants);
  11746. this.edges[id] = edge;
  11747. }
  11748. }
  11749. this.moving = true;
  11750. this._updateValueRange(edges);
  11751. };
  11752. /**
  11753. * Remove existing edges. Non existing ids will be ignored
  11754. * @param {Number[] | String[]} ids
  11755. * @private
  11756. */
  11757. Graph.prototype._removeEdges = function (ids) {
  11758. var edges = this.edges;
  11759. for (var i = 0, len = ids.length; i < len; i++) {
  11760. var id = ids[i];
  11761. var edge = edges[id];
  11762. if (edge) {
  11763. edge.disconnect();
  11764. delete edges[id];
  11765. }
  11766. }
  11767. this.moving = true;
  11768. this._updateValueRange(edges);
  11769. };
  11770. /**
  11771. * Reconnect all edges
  11772. * @private
  11773. */
  11774. Graph.prototype._reconnectEdges = function() {
  11775. var id,
  11776. nodes = this.nodes,
  11777. edges = this.edges;
  11778. for (id in nodes) {
  11779. if (nodes.hasOwnProperty(id)) {
  11780. nodes[id].edges = [];
  11781. }
  11782. }
  11783. for (id in edges) {
  11784. if (edges.hasOwnProperty(id)) {
  11785. var edge = edges[id];
  11786. edge.from = null;
  11787. edge.to = null;
  11788. edge.connect();
  11789. }
  11790. }
  11791. };
  11792. /**
  11793. * Update the values of all object in the given array according to the current
  11794. * value range of the objects in the array.
  11795. * @param {Object} obj An object containing a set of Edges or Nodes
  11796. * The objects must have a method getValue() and
  11797. * setValueRange(min, max).
  11798. * @private
  11799. */
  11800. Graph.prototype._updateValueRange = function(obj) {
  11801. var id;
  11802. // determine the range of the objects
  11803. var valueMin = undefined;
  11804. var valueMax = undefined;
  11805. for (id in obj) {
  11806. if (obj.hasOwnProperty(id)) {
  11807. var value = obj[id].getValue();
  11808. if (value !== undefined) {
  11809. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  11810. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  11811. }
  11812. }
  11813. }
  11814. // adjust the range of all objects
  11815. if (valueMin !== undefined && valueMax !== undefined) {
  11816. for (id in obj) {
  11817. if (obj.hasOwnProperty(id)) {
  11818. obj[id].setValueRange(valueMin, valueMax);
  11819. }
  11820. }
  11821. }
  11822. };
  11823. /**
  11824. * Redraw the graph with the current data
  11825. * chart will be resized too.
  11826. */
  11827. Graph.prototype.redraw = function() {
  11828. this.setSize(this.width, this.height);
  11829. this._redraw();
  11830. };
  11831. /**
  11832. * Redraw the graph with the current data
  11833. * @private
  11834. */
  11835. Graph.prototype._redraw = function() {
  11836. var ctx = this.frame.canvas.getContext('2d');
  11837. // clear the canvas
  11838. var w = this.frame.canvas.width;
  11839. var h = this.frame.canvas.height;
  11840. ctx.clearRect(0, 0, w, h);
  11841. // set scaling and translation
  11842. ctx.save();
  11843. ctx.translate(this.translation.x, this.translation.y);
  11844. ctx.scale(this.scale, this.scale);
  11845. this._doInAllSectors("_drawAllSectorNodes",ctx);
  11846. this._doInAllSectors("_drawEdges",ctx);
  11847. this._doInAllSectors("_drawNodes",ctx);
  11848. // restore original scaling and translation
  11849. ctx.restore();
  11850. };
  11851. /**
  11852. * Set the translation of the graph
  11853. * @param {Number} offsetX Horizontal offset
  11854. * @param {Number} offsetY Vertical offset
  11855. * @private
  11856. */
  11857. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  11858. if (this.translation === undefined) {
  11859. this.translation = {
  11860. x: 0,
  11861. y: 0
  11862. };
  11863. }
  11864. if (offsetX !== undefined) {
  11865. this.translation.x = offsetX;
  11866. }
  11867. if (offsetY !== undefined) {
  11868. this.translation.y = offsetY;
  11869. }
  11870. };
  11871. /**
  11872. * Get the translation of the graph
  11873. * @return {Object} translation An object with parameters x and y, both a number
  11874. * @private
  11875. */
  11876. Graph.prototype._getTranslation = function() {
  11877. return {
  11878. x: this.translation.x,
  11879. y: this.translation.y
  11880. };
  11881. };
  11882. /**
  11883. * Scale the graph
  11884. * @param {Number} scale Scaling factor 1.0 is unscaled
  11885. * @private
  11886. */
  11887. Graph.prototype._setScale = function(scale) {
  11888. this.scale = scale;
  11889. };
  11890. /**
  11891. * Get the current scale of the graph
  11892. * @return {Number} scale Scaling factor 1.0 is unscaled
  11893. * @private
  11894. */
  11895. Graph.prototype._getScale = function() {
  11896. return this.scale;
  11897. };
  11898. /**
  11899. * Convert a horizontal point on the HTML canvas to the x-value of the model
  11900. * @param {number} x
  11901. * @returns {number}
  11902. * @private
  11903. */
  11904. Graph.prototype._canvasToX = function(x) {
  11905. return (x - this.translation.x) / this.scale;
  11906. };
  11907. /**
  11908. * Convert an x-value in the model to a horizontal point on the HTML canvas
  11909. * @param {number} x
  11910. * @returns {number}
  11911. * @private
  11912. */
  11913. Graph.prototype._xToCanvas = function(x) {
  11914. return x * this.scale + this.translation.x;
  11915. };
  11916. /**
  11917. * Convert a vertical point on the HTML canvas to the y-value of the model
  11918. * @param {number} y
  11919. * @returns {number}
  11920. * @private
  11921. */
  11922. Graph.prototype._canvasToY = function(y) {
  11923. return (y - this.translation.y) / this.scale;
  11924. };
  11925. /**
  11926. * Convert an y-value in the model to a vertical point on the HTML canvas
  11927. * @param {number} y
  11928. * @returns {number}
  11929. * @private
  11930. */
  11931. Graph.prototype._yToCanvas = function(y) {
  11932. return y * this.scale + this.translation.y ;
  11933. };
  11934. /**
  11935. * Redraw all nodes
  11936. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  11937. * @param {CanvasRenderingContext2D} ctx
  11938. * @private
  11939. */
  11940. Graph.prototype._drawNodes = function(ctx) {
  11941. // first draw the unselected nodes
  11942. var nodes = this.nodes;
  11943. var selected = [];
  11944. for (var id in nodes) {
  11945. if (nodes.hasOwnProperty(id)) {
  11946. nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
  11947. if (nodes[id].isSelected()) {
  11948. selected.push(id);
  11949. }
  11950. else {
  11951. if (nodes[id].inArea()) {
  11952. nodes[id].draw(ctx);
  11953. }
  11954. }
  11955. }
  11956. }
  11957. // draw the selected nodes on top
  11958. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  11959. if (nodes[selected[s]].inArea()) {
  11960. nodes[selected[s]].draw(ctx);
  11961. }
  11962. }
  11963. };
  11964. /**
  11965. * Redraw all edges
  11966. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  11967. * @param {CanvasRenderingContext2D} ctx
  11968. * @private
  11969. */
  11970. Graph.prototype._drawEdges = function(ctx) {
  11971. var edges = this.edges;
  11972. for (var id in edges) {
  11973. if (edges.hasOwnProperty(id)) {
  11974. var edge = edges[id];
  11975. edge.setScale(this.scale);
  11976. if (edge.connected) {
  11977. edges[id].draw(ctx);
  11978. }
  11979. }
  11980. }
  11981. };
  11982. /**
  11983. * Find a stable position for all nodes
  11984. * @private
  11985. */
  11986. Graph.prototype._doStabilize = function() {
  11987. //var start = new Date();
  11988. // find stable position
  11989. var count = 0;
  11990. var vmin = this.constants.minVelocity;
  11991. var stable = false;
  11992. while (!stable && count < this.constants.maxIterations) {
  11993. this._calculateForces();
  11994. this._discreteStepNodes();
  11995. stable = !this._isMoving(vmin);
  11996. count++;
  11997. }
  11998. // var end = new Date();
  11999. // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
  12000. };
  12001. /**
  12002. * Calculate the external forces acting on the nodes
  12003. * Forces are caused by: edges, repulsing forces between nodes, gravity
  12004. * @private
  12005. */
  12006. Graph.prototype._calculateForces = function(nodes,edges) {
  12007. // stop calculation if there is only one node
  12008. if (this.nodeIndices.length == 1) {
  12009. this.nodes[this.nodeIndices[0]]._setForce(0,0);
  12010. }
  12011. // if there are too many nodes on screen, we cluster without repositioning
  12012. else if (this.nodeIndices.length > this.constants.clustering.maxNumberOfNodes * 4 && this.constants.clustering.enableClustering == true) {
  12013. this.clusterToFit(this.constants.clustering.maxNumberOfNodes * 2, false);
  12014. this._calculateForces();
  12015. }
  12016. else {
  12017. this.canvasTopLeft = {"x": (0-this.translation.x)/this.scale,
  12018. "y": (0-this.translation.y)/this.scale};
  12019. this.canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale,
  12020. "y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale};
  12021. var centerPos = {"x":0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x),
  12022. "y":0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y)}
  12023. // create a local edge to the nodes and edges, that is faster
  12024. var dx, dy, angle, distance, fx, fy,
  12025. repulsingForce, springForce, length, edgeLength,
  12026. node, node1, node2, edge, edgeID, i, j, nodeID, xCenter, yCenter;
  12027. var clusterSize;
  12028. var nodes = this.nodes;
  12029. var edges = this.edges;
  12030. // Gravity is required to keep separated groups from floating off
  12031. // the forces are reset to zero in this loop by using _setForce instead
  12032. // of _addForce
  12033. var gravity = 0.08;
  12034. for (i = 0; i < this.nodeIndices.length; i++) {
  12035. node = nodes[this.nodeIndices[i]];
  12036. // gravity does not apply when we are in a pocket sector
  12037. if (this._sector() == "default") {
  12038. dx = -node.x + centerPos.x;
  12039. dy = -node.y + centerPos.y;
  12040. angle = Math.atan2(dy, dx);
  12041. fx = Math.cos(angle) * gravity;
  12042. fy = Math.sin(angle) * gravity;
  12043. }
  12044. else {
  12045. fx = 0;
  12046. fy = 0;
  12047. }
  12048. node._setForce(fx, fy);
  12049. node.updateDamping(this.nodeIndices.length);
  12050. }
  12051. this.updateLabels();
  12052. // repulsing forces between nodes
  12053. var minimumDistance = this.constants.nodes.distance,
  12054. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  12055. // we loop from i over all but the last entree in the array
  12056. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  12057. for (i = 0; i < this.nodeIndices.length-1; i++) {
  12058. node1 = nodes[this.nodeIndices[i]];
  12059. for (j = i+1; j < this.nodeIndices.length; j++) {
  12060. node2 = nodes[this.nodeIndices[j]];
  12061. clusterSize = (node1.clusterSize + node2.clusterSize - 2);
  12062. dx = node2.x - node1.x;
  12063. dy = node2.y - node1.y;
  12064. distance = Math.sqrt(dx * dx + dy * dy);
  12065. // clusters have a larger region of influence
  12066. minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
  12067. if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
  12068. angle = Math.atan2(dy, dx);
  12069. if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
  12070. repulsingForce = 1.0;
  12071. }
  12072. else {
  12073. // TODO: correct factor for repulsing force
  12074. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12075. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12076. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  12077. }
  12078. // amplify the repulsion for clusters.
  12079. repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
  12080. fx = Math.cos(angle) * repulsingForce;
  12081. fy = Math.sin(angle) * repulsingForce;
  12082. node1._addForce(-fx, -fy);
  12083. node2._addForce(fx, fy);
  12084. }
  12085. }
  12086. }
  12087. /*
  12088. // repulsion of the edges on the nodes and
  12089. for (var nodeID in nodes) {
  12090. if (nodes.hasOwnProperty(nodeID)) {
  12091. node = nodes[nodeID];
  12092. for(var edgeID in edges) {
  12093. if (edges.hasOwnProperty(edgeID)) {
  12094. edge = edges[edgeID];
  12095. // get the center of the edge
  12096. xCenter = edge.from.x+(edge.to.x - edge.from.x)/2;
  12097. yCenter = edge.from.y+(edge.to.y - edge.from.y)/2;
  12098. // calculate normally distributed force
  12099. dx = node.x - xCenter;
  12100. dy = node.y - yCenter;
  12101. distance = Math.sqrt(dx * dx + dy * dy);
  12102. if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
  12103. angle = Math.atan2(dy, dx);
  12104. if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
  12105. repulsingForce = 1.0;
  12106. }
  12107. else {
  12108. // TODO: correct factor for repulsing force
  12109. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12110. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  12111. repulsingForce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)); // TODO: customize the repulsing force
  12112. }
  12113. fx = Math.cos(angle) * repulsingForce;
  12114. fy = Math.sin(angle) * repulsingForce;
  12115. node._addForce(fx, fy);
  12116. edge.from._addForce(-fx/2,-fy/2);
  12117. edge.to._addForce(-fx/2,-fy/2);
  12118. }
  12119. }
  12120. }
  12121. }
  12122. }
  12123. */
  12124. // forces caused by the edges, modelled as springs
  12125. for (edgeID in edges) {
  12126. if (edges.hasOwnProperty(edgeID)) {
  12127. edge = edges[edgeID];
  12128. if (edge.connected) {
  12129. // only calculate forces if nodes are in the same sector
  12130. if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
  12131. clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
  12132. dx = (edge.to.x - edge.from.x);
  12133. dy = (edge.to.y - edge.from.y);
  12134. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  12135. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  12136. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  12137. edgeLength = edge.length;
  12138. // this implies that the edges between big clusters are longer
  12139. edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
  12140. length = Math.sqrt(dx * dx + dy * dy);
  12141. angle = Math.atan2(dy, dx);
  12142. springForce = edge.stiffness * (edgeLength - length);
  12143. fx = Math.cos(angle) * springForce;
  12144. fy = Math.sin(angle) * springForce;
  12145. edge.from._addForce(-fx, -fy);
  12146. edge.to._addForce(fx, fy);
  12147. }
  12148. }
  12149. }
  12150. }
  12151. /*
  12152. // TODO: re-implement repulsion of edges
  12153. // repulsing forces between edges
  12154. var minimumDistance = this.constants.edges.distance,
  12155. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  12156. for (var l = 0; l < edges.length; l++) {
  12157. //Keep distance from other edge centers
  12158. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  12159. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  12160. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  12161. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  12162. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  12163. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  12164. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  12165. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  12166. // calculate normally distributed force
  12167. dx = l2x - lx,
  12168. dy = l2y - ly,
  12169. distance = Math.sqrt(dx * dx + dy * dy),
  12170. angle = Math.atan2(dy, dx),
  12171. // TODO: correct factor for repulsing force
  12172. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12173. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  12174. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  12175. fx = Math.cos(angle) * repulsingforce,
  12176. fy = Math.sin(angle) * repulsingforce;
  12177. edges[l].from._addForce(-fx, -fy);
  12178. edges[l].to._addForce(-fx, -fy);
  12179. edges[l2].from._addForce(fx, fy);
  12180. edges[l2].to._addForce(fx, fy);
  12181. }
  12182. }
  12183. */
  12184. }
  12185. };
  12186. /**
  12187. * Check if any of the nodes is still moving
  12188. * @param {number} vmin the minimum velocity considered as 'moving'
  12189. * @return {boolean} true if moving, false if non of the nodes is moving
  12190. * @private
  12191. */
  12192. Graph.prototype._isMoving = function(vmin) {
  12193. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  12194. var nodes = this.nodes;
  12195. for (var id in nodes) {
  12196. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  12197. return true;
  12198. }
  12199. }
  12200. return false;
  12201. };
  12202. /**
  12203. * Perform one discrete step for all nodes
  12204. * @private
  12205. */
  12206. Graph.prototype._discreteStepNodes = function() {
  12207. var interval = this.refreshRate / 1000.0; // in seconds
  12208. var nodes = this.nodes;
  12209. for (var id in nodes) {
  12210. if (nodes.hasOwnProperty(id)) {
  12211. nodes[id].discreteStep(interval);
  12212. }
  12213. }
  12214. var vmin = this.constants.minVelocity;
  12215. this.moving = this._isMoving(vmin);
  12216. };
  12217. /**
  12218. * Start animating nodes and edges
  12219. */
  12220. Graph.prototype.start = function() {
  12221. if (!this.freezeSimulation) {
  12222. if (this.moving) {
  12223. this._doInAllActiveSectors("_calculateForces");
  12224. this._doInAllActiveSectors("_discreteStepNodes");
  12225. }
  12226. if (this.moving) {
  12227. // start animation. only start timer if it is not already running
  12228. if (!this.timer) {
  12229. var graph = this;
  12230. this.timer = window.setTimeout(function () {
  12231. graph.timer = undefined;
  12232. graph.start();
  12233. graph.start();
  12234. graph._redraw();
  12235. // var start = window.performance.now();
  12236. // graph._redraw();
  12237. // var end = window.performance.now();
  12238. // var time = end - start;
  12239. // console.log('Drawing time: ' + time);
  12240. }, this.refreshRate);
  12241. }
  12242. }
  12243. else {
  12244. this._redraw();
  12245. }
  12246. }
  12247. };
  12248. Graph.prototype.singleStep = function() {
  12249. if (this.moving) {
  12250. this._calculateForces();
  12251. this._discreteStepNodes();
  12252. var vmin = this.constants.minVelocity;
  12253. this.moving = this._isMoving(vmin);
  12254. this._redraw();
  12255. }
  12256. };
  12257. /**
  12258. * Freeze the animation
  12259. */
  12260. Graph.prototype.toggleFreeze = function() {
  12261. if (this.freezeSimulation == false) {
  12262. this.freezeSimulation = true;
  12263. }
  12264. else {
  12265. this.freezeSimulation = false;
  12266. this.start();
  12267. }
  12268. };
  12269. Graph.prototype._loadSectorSystem = function() {
  12270. this.sectors = {};
  12271. this.activeSector = ["default"];
  12272. this.sectors["active"] = {};
  12273. this.sectors["active"][this.activeSector[this.activeSector.length-1]] = {"nodes":{},
  12274. "edges":{},
  12275. "nodeIndices":[],
  12276. "formationScale": 1.0,
  12277. "drawingNode": undefined};
  12278. this.sectors["frozen"] = {};
  12279. this.nodeIndices = this.sectors["active"][this.activeSector[this.activeSector.length-1]]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
  12280. for (var mixinFunction in SectorMixin) {
  12281. if (SectorMixin.hasOwnProperty(mixinFunction)) {
  12282. Graph.prototype[mixinFunction] = SectorMixin[mixinFunction];
  12283. }
  12284. }
  12285. };
  12286. /**
  12287. * vis.js module exports
  12288. */
  12289. var vis = {
  12290. util: util,
  12291. events: events,
  12292. Controller: Controller,
  12293. DataSet: DataSet,
  12294. DataView: DataView,
  12295. Range: Range,
  12296. Stack: Stack,
  12297. TimeStep: TimeStep,
  12298. EventBus: EventBus,
  12299. components: {
  12300. items: {
  12301. Item: Item,
  12302. ItemBox: ItemBox,
  12303. ItemPoint: ItemPoint,
  12304. ItemRange: ItemRange
  12305. },
  12306. Component: Component,
  12307. Panel: Panel,
  12308. RootPanel: RootPanel,
  12309. ItemSet: ItemSet,
  12310. TimeAxis: TimeAxis
  12311. },
  12312. graph: {
  12313. Node: Node,
  12314. Edge: Edge,
  12315. Popup: Popup,
  12316. Groups: Groups,
  12317. Images: Images
  12318. },
  12319. Timeline: Timeline,
  12320. Graph: Graph
  12321. };
  12322. /**
  12323. * CommonJS module exports
  12324. */
  12325. if (typeof exports !== 'undefined') {
  12326. exports = vis;
  12327. }
  12328. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  12329. module.exports = vis;
  12330. }
  12331. /**
  12332. * AMD module exports
  12333. */
  12334. if (typeof(define) === 'function') {
  12335. define(function () {
  12336. return vis;
  12337. });
  12338. }
  12339. /**
  12340. * Window exports
  12341. */
  12342. if (typeof window !== 'undefined') {
  12343. // attach the module to the window, load as a regular javascript file
  12344. window['vis'] = vis;
  12345. }
  12346. },{"hammerjs":2,"moment":3,"mouseTrap":4}],2:[function(require,module,exports){
  12347. /*! Hammer.JS - v1.0.5 - 2013-04-07
  12348. * http://eightmedia.github.com/hammer.js
  12349. *
  12350. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  12351. * Licensed under the MIT license */
  12352. (function(window, undefined) {
  12353. 'use strict';
  12354. /**
  12355. * Hammer
  12356. * use this to create instances
  12357. * @param {HTMLElement} element
  12358. * @param {Object} options
  12359. * @returns {Hammer.Instance}
  12360. * @constructor
  12361. */
  12362. var Hammer = function(element, options) {
  12363. return new Hammer.Instance(element, options || {});
  12364. };
  12365. // default settings
  12366. Hammer.defaults = {
  12367. // add styles and attributes to the element to prevent the browser from doing
  12368. // its native behavior. this doesnt prevent the scrolling, but cancels
  12369. // the contextmenu, tap highlighting etc
  12370. // set to false to disable this
  12371. stop_browser_behavior: {
  12372. // this also triggers onselectstart=false for IE
  12373. userSelect: 'none',
  12374. // this makes the element blocking in IE10 >, you could experiment with the value
  12375. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  12376. touchAction: 'none',
  12377. touchCallout: 'none',
  12378. contentZooming: 'none',
  12379. userDrag: 'none',
  12380. tapHighlightColor: 'rgba(0,0,0,0)'
  12381. }
  12382. // more settings are defined per gesture at gestures.js
  12383. };
  12384. // detect touchevents
  12385. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  12386. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  12387. // dont use mouseevents on mobile devices
  12388. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  12389. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  12390. // eventtypes per touchevent (start, move, end)
  12391. // are filled by Hammer.event.determineEventTypes on setup
  12392. Hammer.EVENT_TYPES = {};
  12393. // direction defines
  12394. Hammer.DIRECTION_DOWN = 'down';
  12395. Hammer.DIRECTION_LEFT = 'left';
  12396. Hammer.DIRECTION_UP = 'up';
  12397. Hammer.DIRECTION_RIGHT = 'right';
  12398. // pointer type
  12399. Hammer.POINTER_MOUSE = 'mouse';
  12400. Hammer.POINTER_TOUCH = 'touch';
  12401. Hammer.POINTER_PEN = 'pen';
  12402. // touch event defines
  12403. Hammer.EVENT_START = 'start';
  12404. Hammer.EVENT_MOVE = 'move';
  12405. Hammer.EVENT_END = 'end';
  12406. // hammer document where the base events are added at
  12407. Hammer.DOCUMENT = document;
  12408. // plugins namespace
  12409. Hammer.plugins = {};
  12410. // if the window events are set...
  12411. Hammer.READY = false;
  12412. /**
  12413. * setup events to detect gestures on the document
  12414. */
  12415. function setup() {
  12416. if(Hammer.READY) {
  12417. return;
  12418. }
  12419. // find what eventtypes we add listeners to
  12420. Hammer.event.determineEventTypes();
  12421. // Register all gestures inside Hammer.gestures
  12422. for(var name in Hammer.gestures) {
  12423. if(Hammer.gestures.hasOwnProperty(name)) {
  12424. Hammer.detection.register(Hammer.gestures[name]);
  12425. }
  12426. }
  12427. // Add touch events on the document
  12428. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  12429. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  12430. // Hammer is ready...!
  12431. Hammer.READY = true;
  12432. }
  12433. /**
  12434. * create new hammer instance
  12435. * all methods should return the instance itself, so it is chainable.
  12436. * @param {HTMLElement} element
  12437. * @param {Object} [options={}]
  12438. * @returns {Hammer.Instance}
  12439. * @constructor
  12440. */
  12441. Hammer.Instance = function(element, options) {
  12442. var self = this;
  12443. // setup HammerJS window events and register all gestures
  12444. // this also sets up the default options
  12445. setup();
  12446. this.element = element;
  12447. // start/stop detection option
  12448. this.enabled = true;
  12449. // merge options
  12450. this.options = Hammer.utils.extend(
  12451. Hammer.utils.extend({}, Hammer.defaults),
  12452. options || {});
  12453. // add some css to the element to prevent the browser from doing its native behavoir
  12454. if(this.options.stop_browser_behavior) {
  12455. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  12456. }
  12457. // start detection on touchstart
  12458. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  12459. if(self.enabled) {
  12460. Hammer.detection.startDetect(self, ev);
  12461. }
  12462. });
  12463. // return instance
  12464. return this;
  12465. };
  12466. Hammer.Instance.prototype = {
  12467. /**
  12468. * bind events to the instance
  12469. * @param {String} gesture
  12470. * @param {Function} handler
  12471. * @returns {Hammer.Instance}
  12472. */
  12473. on: function onEvent(gesture, handler){
  12474. var gestures = gesture.split(' ');
  12475. for(var t=0; t<gestures.length; t++) {
  12476. this.element.addEventListener(gestures[t], handler, false);
  12477. }
  12478. return this;
  12479. },
  12480. /**
  12481. * unbind events to the instance
  12482. * @param {String} gesture
  12483. * @param {Function} handler
  12484. * @returns {Hammer.Instance}
  12485. */
  12486. off: function offEvent(gesture, handler){
  12487. var gestures = gesture.split(' ');
  12488. for(var t=0; t<gestures.length; t++) {
  12489. this.element.removeEventListener(gestures[t], handler, false);
  12490. }
  12491. return this;
  12492. },
  12493. /**
  12494. * trigger gesture event
  12495. * @param {String} gesture
  12496. * @param {Object} eventData
  12497. * @returns {Hammer.Instance}
  12498. */
  12499. trigger: function triggerEvent(gesture, eventData){
  12500. // create DOM event
  12501. var event = Hammer.DOCUMENT.createEvent('Event');
  12502. event.initEvent(gesture, true, true);
  12503. event.gesture = eventData;
  12504. // trigger on the target if it is in the instance element,
  12505. // this is for event delegation tricks
  12506. var element = this.element;
  12507. if(Hammer.utils.hasParent(eventData.target, element)) {
  12508. element = eventData.target;
  12509. }
  12510. element.dispatchEvent(event);
  12511. return this;
  12512. },
  12513. /**
  12514. * enable of disable hammer.js detection
  12515. * @param {Boolean} state
  12516. * @returns {Hammer.Instance}
  12517. */
  12518. enable: function enable(state) {
  12519. this.enabled = state;
  12520. return this;
  12521. }
  12522. };
  12523. /**
  12524. * this holds the last move event,
  12525. * used to fix empty touchend issue
  12526. * see the onTouch event for an explanation
  12527. * @type {Object}
  12528. */
  12529. var last_move_event = null;
  12530. /**
  12531. * when the mouse is hold down, this is true
  12532. * @type {Boolean}
  12533. */
  12534. var enable_detect = false;
  12535. /**
  12536. * when touch events have been fired, this is true
  12537. * @type {Boolean}
  12538. */
  12539. var touch_triggered = false;
  12540. Hammer.event = {
  12541. /**
  12542. * simple addEventListener
  12543. * @param {HTMLElement} element
  12544. * @param {String} type
  12545. * @param {Function} handler
  12546. */
  12547. bindDom: function(element, type, handler) {
  12548. var types = type.split(' ');
  12549. for(var t=0; t<types.length; t++) {
  12550. element.addEventListener(types[t], handler, false);
  12551. }
  12552. },
  12553. /**
  12554. * touch events with mouse fallback
  12555. * @param {HTMLElement} element
  12556. * @param {String} eventType like Hammer.EVENT_MOVE
  12557. * @param {Function} handler
  12558. */
  12559. onTouch: function onTouch(element, eventType, handler) {
  12560. var self = this;
  12561. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  12562. var sourceEventType = ev.type.toLowerCase();
  12563. // onmouseup, but when touchend has been fired we do nothing.
  12564. // this is for touchdevices which also fire a mouseup on touchend
  12565. if(sourceEventType.match(/mouse/) && touch_triggered) {
  12566. return;
  12567. }
  12568. // mousebutton must be down or a touch event
  12569. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  12570. sourceEventType.match(/pointerdown/) || // pointerevents touch
  12571. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  12572. ){
  12573. enable_detect = true;
  12574. }
  12575. // we are in a touch event, set the touch triggered bool to true,
  12576. // this for the conflicts that may occur on ios and android
  12577. if(sourceEventType.match(/touch|pointer/)) {
  12578. touch_triggered = true;
  12579. }
  12580. // count the total touches on the screen
  12581. var count_touches = 0;
  12582. // when touch has been triggered in this detection session
  12583. // and we are now handling a mouse event, we stop that to prevent conflicts
  12584. if(enable_detect) {
  12585. // update pointerevent
  12586. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  12587. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  12588. }
  12589. // touch
  12590. else if(sourceEventType.match(/touch/)) {
  12591. count_touches = ev.touches.length;
  12592. }
  12593. // mouse
  12594. else if(!touch_triggered) {
  12595. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  12596. }
  12597. // if we are in a end event, but when we remove one touch and
  12598. // we still have enough, set eventType to move
  12599. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  12600. eventType = Hammer.EVENT_MOVE;
  12601. }
  12602. // no touches, force the end event
  12603. else if(!count_touches) {
  12604. eventType = Hammer.EVENT_END;
  12605. }
  12606. // because touchend has no touches, and we often want to use these in our gestures,
  12607. // we send the last move event as our eventData in touchend
  12608. if(!count_touches && last_move_event !== null) {
  12609. ev = last_move_event;
  12610. }
  12611. // store the last move event
  12612. else {
  12613. last_move_event = ev;
  12614. }
  12615. // trigger the handler
  12616. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  12617. // remove pointerevent from list
  12618. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  12619. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  12620. }
  12621. }
  12622. //debug(sourceEventType +" "+ eventType);
  12623. // on the end we reset everything
  12624. if(!count_touches) {
  12625. last_move_event = null;
  12626. enable_detect = false;
  12627. touch_triggered = false;
  12628. Hammer.PointerEvent.reset();
  12629. }
  12630. });
  12631. },
  12632. /**
  12633. * we have different events for each device/browser
  12634. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  12635. */
  12636. determineEventTypes: function determineEventTypes() {
  12637. // determine the eventtype we want to set
  12638. var types;
  12639. // pointerEvents magic
  12640. if(Hammer.HAS_POINTEREVENTS) {
  12641. types = Hammer.PointerEvent.getEvents();
  12642. }
  12643. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  12644. else if(Hammer.NO_MOUSEEVENTS) {
  12645. types = [
  12646. 'touchstart',
  12647. 'touchmove',
  12648. 'touchend touchcancel'];
  12649. }
  12650. // for non pointer events browsers and mixed browsers,
  12651. // like chrome on windows8 touch laptop
  12652. else {
  12653. types = [
  12654. 'touchstart mousedown',
  12655. 'touchmove mousemove',
  12656. 'touchend touchcancel mouseup'];
  12657. }
  12658. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  12659. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  12660. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  12661. },
  12662. /**
  12663. * create touchlist depending on the event
  12664. * @param {Object} ev
  12665. * @param {String} eventType used by the fakemultitouch plugin
  12666. */
  12667. getTouchList: function getTouchList(ev/*, eventType*/) {
  12668. // get the fake pointerEvent touchlist
  12669. if(Hammer.HAS_POINTEREVENTS) {
  12670. return Hammer.PointerEvent.getTouchList();
  12671. }
  12672. // get the touchlist
  12673. else if(ev.touches) {
  12674. return ev.touches;
  12675. }
  12676. // make fake touchlist from mouse position
  12677. else {
  12678. return [{
  12679. identifier: 1,
  12680. pageX: ev.pageX,
  12681. pageY: ev.pageY,
  12682. target: ev.target
  12683. }];
  12684. }
  12685. },
  12686. /**
  12687. * collect event data for Hammer js
  12688. * @param {HTMLElement} element
  12689. * @param {String} eventType like Hammer.EVENT_MOVE
  12690. * @param {Object} eventData
  12691. */
  12692. collectEventData: function collectEventData(element, eventType, ev) {
  12693. var touches = this.getTouchList(ev, eventType);
  12694. // find out pointerType
  12695. var pointerType = Hammer.POINTER_TOUCH;
  12696. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  12697. pointerType = Hammer.POINTER_MOUSE;
  12698. }
  12699. return {
  12700. center : Hammer.utils.getCenter(touches),
  12701. timeStamp : new Date().getTime(),
  12702. target : ev.target,
  12703. touches : touches,
  12704. eventType : eventType,
  12705. pointerType : pointerType,
  12706. srcEvent : ev,
  12707. /**
  12708. * prevent the browser default actions
  12709. * mostly used to disable scrolling of the browser
  12710. */
  12711. preventDefault: function() {
  12712. if(this.srcEvent.preventManipulation) {
  12713. this.srcEvent.preventManipulation();
  12714. }
  12715. if(this.srcEvent.preventDefault) {
  12716. this.srcEvent.preventDefault();
  12717. }
  12718. },
  12719. /**
  12720. * stop bubbling the event up to its parents
  12721. */
  12722. stopPropagation: function() {
  12723. this.srcEvent.stopPropagation();
  12724. },
  12725. /**
  12726. * immediately stop gesture detection
  12727. * might be useful after a swipe was detected
  12728. * @return {*}
  12729. */
  12730. stopDetect: function() {
  12731. return Hammer.detection.stopDetect();
  12732. }
  12733. };
  12734. }
  12735. };
  12736. Hammer.PointerEvent = {
  12737. /**
  12738. * holds all pointers
  12739. * @type {Object}
  12740. */
  12741. pointers: {},
  12742. /**
  12743. * get a list of pointers
  12744. * @returns {Array} touchlist
  12745. */
  12746. getTouchList: function() {
  12747. var self = this;
  12748. var touchlist = [];
  12749. // we can use forEach since pointerEvents only is in IE10
  12750. Object.keys(self.pointers).sort().forEach(function(id) {
  12751. touchlist.push(self.pointers[id]);
  12752. });
  12753. return touchlist;
  12754. },
  12755. /**
  12756. * update the position of a pointer
  12757. * @param {String} type Hammer.EVENT_END
  12758. * @param {Object} pointerEvent
  12759. */
  12760. updatePointer: function(type, pointerEvent) {
  12761. if(type == Hammer.EVENT_END) {
  12762. this.pointers = {};
  12763. }
  12764. else {
  12765. pointerEvent.identifier = pointerEvent.pointerId;
  12766. this.pointers[pointerEvent.pointerId] = pointerEvent;
  12767. }
  12768. return Object.keys(this.pointers).length;
  12769. },
  12770. /**
  12771. * check if ev matches pointertype
  12772. * @param {String} pointerType Hammer.POINTER_MOUSE
  12773. * @param {PointerEvent} ev
  12774. */
  12775. matchType: function(pointerType, ev) {
  12776. if(!ev.pointerType) {
  12777. return false;
  12778. }
  12779. var types = {};
  12780. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  12781. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  12782. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  12783. return types[pointerType];
  12784. },
  12785. /**
  12786. * get events
  12787. */
  12788. getEvents: function() {
  12789. return [
  12790. 'pointerdown MSPointerDown',
  12791. 'pointermove MSPointerMove',
  12792. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  12793. ];
  12794. },
  12795. /**
  12796. * reset the list
  12797. */
  12798. reset: function() {
  12799. this.pointers = {};
  12800. }
  12801. };
  12802. Hammer.utils = {
  12803. /**
  12804. * extend method,
  12805. * also used for cloning when dest is an empty object
  12806. * @param {Object} dest
  12807. * @param {Object} src
  12808. * @parm {Boolean} merge do a merge
  12809. * @returns {Object} dest
  12810. */
  12811. extend: function extend(dest, src, merge) {
  12812. for (var key in src) {
  12813. if(dest[key] !== undefined && merge) {
  12814. continue;
  12815. }
  12816. dest[key] = src[key];
  12817. }
  12818. return dest;
  12819. },
  12820. /**
  12821. * find if a node is in the given parent
  12822. * used for event delegation tricks
  12823. * @param {HTMLElement} node
  12824. * @param {HTMLElement} parent
  12825. * @returns {boolean} has_parent
  12826. */
  12827. hasParent: function(node, parent) {
  12828. while(node){
  12829. if(node == parent) {
  12830. return true;
  12831. }
  12832. node = node.parentNode;
  12833. }
  12834. return false;
  12835. },
  12836. /**
  12837. * get the center of all the touches
  12838. * @param {Array} touches
  12839. * @returns {Object} center
  12840. */
  12841. getCenter: function getCenter(touches) {
  12842. var valuesX = [], valuesY = [];
  12843. for(var t= 0,len=touches.length; t<len; t++) {
  12844. valuesX.push(touches[t].pageX);
  12845. valuesY.push(touches[t].pageY);
  12846. }
  12847. return {
  12848. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  12849. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  12850. };
  12851. },
  12852. /**
  12853. * calculate the velocity between two points
  12854. * @param {Number} delta_time
  12855. * @param {Number} delta_x
  12856. * @param {Number} delta_y
  12857. * @returns {Object} velocity
  12858. */
  12859. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  12860. return {
  12861. x: Math.abs(delta_x / delta_time) || 0,
  12862. y: Math.abs(delta_y / delta_time) || 0
  12863. };
  12864. },
  12865. /**
  12866. * calculate the angle between two coordinates
  12867. * @param {Touch} touch1
  12868. * @param {Touch} touch2
  12869. * @returns {Number} angle
  12870. */
  12871. getAngle: function getAngle(touch1, touch2) {
  12872. var y = touch2.pageY - touch1.pageY,
  12873. x = touch2.pageX - touch1.pageX;
  12874. return Math.atan2(y, x) * 180 / Math.PI;
  12875. },
  12876. /**
  12877. * angle to direction define
  12878. * @param {Touch} touch1
  12879. * @param {Touch} touch2
  12880. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  12881. */
  12882. getDirection: function getDirection(touch1, touch2) {
  12883. var x = Math.abs(touch1.pageX - touch2.pageX),
  12884. y = Math.abs(touch1.pageY - touch2.pageY);
  12885. if(x >= y) {
  12886. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  12887. }
  12888. else {
  12889. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  12890. }
  12891. },
  12892. /**
  12893. * calculate the distance between two touches
  12894. * @param {Touch} touch1
  12895. * @param {Touch} touch2
  12896. * @returns {Number} distance
  12897. */
  12898. getDistance: function getDistance(touch1, touch2) {
  12899. var x = touch2.pageX - touch1.pageX,
  12900. y = touch2.pageY - touch1.pageY;
  12901. return Math.sqrt((x*x) + (y*y));
  12902. },
  12903. /**
  12904. * calculate the scale factor between two touchLists (fingers)
  12905. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  12906. * @param {Array} start
  12907. * @param {Array} end
  12908. * @returns {Number} scale
  12909. */
  12910. getScale: function getScale(start, end) {
  12911. // need two fingers...
  12912. if(start.length >= 2 && end.length >= 2) {
  12913. return this.getDistance(end[0], end[1]) /
  12914. this.getDistance(start[0], start[1]);
  12915. }
  12916. return 1;
  12917. },
  12918. /**
  12919. * calculate the rotation degrees between two touchLists (fingers)
  12920. * @param {Array} start
  12921. * @param {Array} end
  12922. * @returns {Number} rotation
  12923. */
  12924. getRotation: function getRotation(start, end) {
  12925. // need two fingers
  12926. if(start.length >= 2 && end.length >= 2) {
  12927. return this.getAngle(end[1], end[0]) -
  12928. this.getAngle(start[1], start[0]);
  12929. }
  12930. return 0;
  12931. },
  12932. /**
  12933. * boolean if the direction is vertical
  12934. * @param {String} direction
  12935. * @returns {Boolean} is_vertical
  12936. */
  12937. isVertical: function isVertical(direction) {
  12938. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  12939. },
  12940. /**
  12941. * stop browser default behavior with css props
  12942. * @param {HtmlElement} element
  12943. * @param {Object} css_props
  12944. */
  12945. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  12946. var prop,
  12947. vendors = ['webkit','khtml','moz','ms','o',''];
  12948. if(!css_props || !element.style) {
  12949. return;
  12950. }
  12951. // with css properties for modern browsers
  12952. for(var i = 0; i < vendors.length; i++) {
  12953. for(var p in css_props) {
  12954. if(css_props.hasOwnProperty(p)) {
  12955. prop = p;
  12956. // vender prefix at the property
  12957. if(vendors[i]) {
  12958. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  12959. }
  12960. // set the style
  12961. element.style[prop] = css_props[p];
  12962. }
  12963. }
  12964. }
  12965. // also the disable onselectstart
  12966. if(css_props.userSelect == 'none') {
  12967. element.onselectstart = function() {
  12968. return false;
  12969. };
  12970. }
  12971. }
  12972. };
  12973. Hammer.detection = {
  12974. // contains all registred Hammer.gestures in the correct order
  12975. gestures: [],
  12976. // data of the current Hammer.gesture detection session
  12977. current: null,
  12978. // the previous Hammer.gesture session data
  12979. // is a full clone of the previous gesture.current object
  12980. previous: null,
  12981. // when this becomes true, no gestures are fired
  12982. stopped: false,
  12983. /**
  12984. * start Hammer.gesture detection
  12985. * @param {Hammer.Instance} inst
  12986. * @param {Object} eventData
  12987. */
  12988. startDetect: function startDetect(inst, eventData) {
  12989. // already busy with a Hammer.gesture detection on an element
  12990. if(this.current) {
  12991. return;
  12992. }
  12993. this.stopped = false;
  12994. this.current = {
  12995. inst : inst, // reference to HammerInstance we're working for
  12996. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  12997. lastEvent : false, // last eventData
  12998. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  12999. };
  13000. this.detect(eventData);
  13001. },
  13002. /**
  13003. * Hammer.gesture detection
  13004. * @param {Object} eventData
  13005. * @param {Object} eventData
  13006. */
  13007. detect: function detect(eventData) {
  13008. if(!this.current || this.stopped) {
  13009. return;
  13010. }
  13011. // extend event data with calculations about scale, distance etc
  13012. eventData = this.extendEventData(eventData);
  13013. // instance options
  13014. var inst_options = this.current.inst.options;
  13015. // call Hammer.gesture handlers
  13016. for(var g=0,len=this.gestures.length; g<len; g++) {
  13017. var gesture = this.gestures[g];
  13018. // only when the instance options have enabled this gesture
  13019. if(!this.stopped && inst_options[gesture.name] !== false) {
  13020. // if a handler returns false, we stop with the detection
  13021. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  13022. this.stopDetect();
  13023. break;
  13024. }
  13025. }
  13026. }
  13027. // store as previous event event
  13028. if(this.current) {
  13029. this.current.lastEvent = eventData;
  13030. }
  13031. // endevent, but not the last touch, so dont stop
  13032. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  13033. this.stopDetect();
  13034. }
  13035. return eventData;
  13036. },
  13037. /**
  13038. * clear the Hammer.gesture vars
  13039. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  13040. * to stop other Hammer.gestures from being fired
  13041. */
  13042. stopDetect: function stopDetect() {
  13043. // clone current data to the store as the previous gesture
  13044. // used for the double tap gesture, since this is an other gesture detect session
  13045. this.previous = Hammer.utils.extend({}, this.current);
  13046. // reset the current
  13047. this.current = null;
  13048. // stopped!
  13049. this.stopped = true;
  13050. },
  13051. /**
  13052. * extend eventData for Hammer.gestures
  13053. * @param {Object} ev
  13054. * @returns {Object} ev
  13055. */
  13056. extendEventData: function extendEventData(ev) {
  13057. var startEv = this.current.startEvent;
  13058. // if the touches change, set the new touches over the startEvent touches
  13059. // this because touchevents don't have all the touches on touchstart, or the
  13060. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  13061. // but, sometimes it happens that both fingers are touching at the EXACT same time
  13062. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  13063. // extend 1 level deep to get the touchlist with the touch objects
  13064. startEv.touches = [];
  13065. for(var i=0,len=ev.touches.length; i<len; i++) {
  13066. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  13067. }
  13068. }
  13069. var delta_time = ev.timeStamp - startEv.timeStamp,
  13070. delta_x = ev.center.pageX - startEv.center.pageX,
  13071. delta_y = ev.center.pageY - startEv.center.pageY,
  13072. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  13073. Hammer.utils.extend(ev, {
  13074. deltaTime : delta_time,
  13075. deltaX : delta_x,
  13076. deltaY : delta_y,
  13077. velocityX : velocity.x,
  13078. velocityY : velocity.y,
  13079. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  13080. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  13081. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  13082. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  13083. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  13084. startEvent : startEv
  13085. });
  13086. return ev;
  13087. },
  13088. /**
  13089. * register new gesture
  13090. * @param {Object} gesture object, see gestures.js for documentation
  13091. * @returns {Array} gestures
  13092. */
  13093. register: function register(gesture) {
  13094. // add an enable gesture options if there is no given
  13095. var options = gesture.defaults || {};
  13096. if(options[gesture.name] === undefined) {
  13097. options[gesture.name] = true;
  13098. }
  13099. // extend Hammer default options with the Hammer.gesture options
  13100. Hammer.utils.extend(Hammer.defaults, options, true);
  13101. // set its index
  13102. gesture.index = gesture.index || 1000;
  13103. // add Hammer.gesture to the list
  13104. this.gestures.push(gesture);
  13105. // sort the list by index
  13106. this.gestures.sort(function(a, b) {
  13107. if (a.index < b.index) {
  13108. return -1;
  13109. }
  13110. if (a.index > b.index) {
  13111. return 1;
  13112. }
  13113. return 0;
  13114. });
  13115. return this.gestures;
  13116. }
  13117. };
  13118. Hammer.gestures = Hammer.gestures || {};
  13119. /**
  13120. * Custom gestures
  13121. * ==============================
  13122. *
  13123. * Gesture object
  13124. * --------------------
  13125. * The object structure of a gesture:
  13126. *
  13127. * { name: 'mygesture',
  13128. * index: 1337,
  13129. * defaults: {
  13130. * mygesture_option: true
  13131. * }
  13132. * handler: function(type, ev, inst) {
  13133. * // trigger gesture event
  13134. * inst.trigger(this.name, ev);
  13135. * }
  13136. * }
  13137. * @param {String} name
  13138. * this should be the name of the gesture, lowercase
  13139. * it is also being used to disable/enable the gesture per instance config.
  13140. *
  13141. * @param {Number} [index=1000]
  13142. * the index of the gesture, where it is going to be in the stack of gestures detection
  13143. * like when you build an gesture that depends on the drag gesture, it is a good
  13144. * idea to place it after the index of the drag gesture.
  13145. *
  13146. * @param {Object} [defaults={}]
  13147. * the default settings of the gesture. these are added to the instance settings,
  13148. * and can be overruled per instance. you can also add the name of the gesture,
  13149. * but this is also added by default (and set to true).
  13150. *
  13151. * @param {Function} handler
  13152. * this handles the gesture detection of your custom gesture and receives the
  13153. * following arguments:
  13154. *
  13155. * @param {Object} eventData
  13156. * event data containing the following properties:
  13157. * timeStamp {Number} time the event occurred
  13158. * target {HTMLElement} target element
  13159. * touches {Array} touches (fingers, pointers, mouse) on the screen
  13160. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  13161. * center {Object} center position of the touches. contains pageX and pageY
  13162. * deltaTime {Number} the total time of the touches in the screen
  13163. * deltaX {Number} the delta on x axis we haved moved
  13164. * deltaY {Number} the delta on y axis we haved moved
  13165. * velocityX {Number} the velocity on the x
  13166. * velocityY {Number} the velocity on y
  13167. * angle {Number} the angle we are moving
  13168. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  13169. * distance {Number} the distance we haved moved
  13170. * scale {Number} scaling of the touches, needs 2 touches
  13171. * rotation {Number} rotation of the touches, needs 2 touches *
  13172. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  13173. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  13174. * startEvent {Object} contains the same properties as above,
  13175. * but from the first touch. this is used to calculate
  13176. * distances, deltaTime, scaling etc
  13177. *
  13178. * @param {Hammer.Instance} inst
  13179. * the instance we are doing the detection for. you can get the options from
  13180. * the inst.options object and trigger the gesture event by calling inst.trigger
  13181. *
  13182. *
  13183. * Handle gestures
  13184. * --------------------
  13185. * inside the handler you can get/set Hammer.detection.current. This is the current
  13186. * detection session. It has the following properties
  13187. * @param {String} name
  13188. * contains the name of the gesture we have detected. it has not a real function,
  13189. * only to check in other gestures if something is detected.
  13190. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  13191. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  13192. *
  13193. * @readonly
  13194. * @param {Hammer.Instance} inst
  13195. * the instance we do the detection for
  13196. *
  13197. * @readonly
  13198. * @param {Object} startEvent
  13199. * contains the properties of the first gesture detection in this session.
  13200. * Used for calculations about timing, distance, etc.
  13201. *
  13202. * @readonly
  13203. * @param {Object} lastEvent
  13204. * contains all the properties of the last gesture detect in this session.
  13205. *
  13206. * after the gesture detection session has been completed (user has released the screen)
  13207. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  13208. * this is usefull for gestures like doubletap, where you need to know if the
  13209. * previous gesture was a tap
  13210. *
  13211. * options that have been set by the instance can be received by calling inst.options
  13212. *
  13213. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  13214. * The first param is the name of your gesture, the second the event argument
  13215. *
  13216. *
  13217. * Register gestures
  13218. * --------------------
  13219. * When an gesture is added to the Hammer.gestures object, it is auto registered
  13220. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  13221. * manually and pass your gesture object as a param
  13222. *
  13223. */
  13224. /**
  13225. * Hold
  13226. * Touch stays at the same place for x time
  13227. * @events hold
  13228. */
  13229. Hammer.gestures.Hold = {
  13230. name: 'hold',
  13231. index: 10,
  13232. defaults: {
  13233. hold_timeout : 500,
  13234. hold_threshold : 1
  13235. },
  13236. timer: null,
  13237. handler: function holdGesture(ev, inst) {
  13238. switch(ev.eventType) {
  13239. case Hammer.EVENT_START:
  13240. // clear any running timers
  13241. clearTimeout(this.timer);
  13242. // set the gesture so we can check in the timeout if it still is
  13243. Hammer.detection.current.name = this.name;
  13244. // set timer and if after the timeout it still is hold,
  13245. // we trigger the hold event
  13246. this.timer = setTimeout(function() {
  13247. if(Hammer.detection.current.name == 'hold') {
  13248. inst.trigger('hold', ev);
  13249. }
  13250. }, inst.options.hold_timeout);
  13251. break;
  13252. // when you move or end we clear the timer
  13253. case Hammer.EVENT_MOVE:
  13254. if(ev.distance > inst.options.hold_threshold) {
  13255. clearTimeout(this.timer);
  13256. }
  13257. break;
  13258. case Hammer.EVENT_END:
  13259. clearTimeout(this.timer);
  13260. break;
  13261. }
  13262. }
  13263. };
  13264. /**
  13265. * Tap/DoubleTap
  13266. * Quick touch at a place or double at the same place
  13267. * @events tap, doubletap
  13268. */
  13269. Hammer.gestures.Tap = {
  13270. name: 'tap',
  13271. index: 100,
  13272. defaults: {
  13273. tap_max_touchtime : 250,
  13274. tap_max_distance : 10,
  13275. tap_always : true,
  13276. doubletap_distance : 20,
  13277. doubletap_interval : 300
  13278. },
  13279. handler: function tapGesture(ev, inst) {
  13280. if(ev.eventType == Hammer.EVENT_END) {
  13281. // previous gesture, for the double tap since these are two different gesture detections
  13282. var prev = Hammer.detection.previous,
  13283. did_doubletap = false;
  13284. // when the touchtime is higher then the max touch time
  13285. // or when the moving distance is too much
  13286. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  13287. ev.distance > inst.options.tap_max_distance) {
  13288. return;
  13289. }
  13290. // check if double tap
  13291. if(prev && prev.name == 'tap' &&
  13292. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  13293. ev.distance < inst.options.doubletap_distance) {
  13294. inst.trigger('doubletap', ev);
  13295. did_doubletap = true;
  13296. }
  13297. // do a single tap
  13298. if(!did_doubletap || inst.options.tap_always) {
  13299. Hammer.detection.current.name = 'tap';
  13300. inst.trigger(Hammer.detection.current.name, ev);
  13301. }
  13302. }
  13303. }
  13304. };
  13305. /**
  13306. * Swipe
  13307. * triggers swipe events when the end velocity is above the threshold
  13308. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  13309. */
  13310. Hammer.gestures.Swipe = {
  13311. name: 'swipe',
  13312. index: 40,
  13313. defaults: {
  13314. // set 0 for unlimited, but this can conflict with transform
  13315. swipe_max_touches : 1,
  13316. swipe_velocity : 0.7
  13317. },
  13318. handler: function swipeGesture(ev, inst) {
  13319. if(ev.eventType == Hammer.EVENT_END) {
  13320. // max touches
  13321. if(inst.options.swipe_max_touches > 0 &&
  13322. ev.touches.length > inst.options.swipe_max_touches) {
  13323. return;
  13324. }
  13325. // when the distance we moved is too small we skip this gesture
  13326. // or we can be already in dragging
  13327. if(ev.velocityX > inst.options.swipe_velocity ||
  13328. ev.velocityY > inst.options.swipe_velocity) {
  13329. // trigger swipe events
  13330. inst.trigger(this.name, ev);
  13331. inst.trigger(this.name + ev.direction, ev);
  13332. }
  13333. }
  13334. }
  13335. };
  13336. /**
  13337. * Drag
  13338. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  13339. * moving left and right is a good practice. When all the drag events are blocking
  13340. * you disable scrolling on that area.
  13341. * @events drag, drapleft, dragright, dragup, dragdown
  13342. */
  13343. Hammer.gestures.Drag = {
  13344. name: 'drag',
  13345. index: 50,
  13346. defaults: {
  13347. drag_min_distance : 10,
  13348. // set 0 for unlimited, but this can conflict with transform
  13349. drag_max_touches : 1,
  13350. // prevent default browser behavior when dragging occurs
  13351. // be careful with it, it makes the element a blocking element
  13352. // when you are using the drag gesture, it is a good practice to set this true
  13353. drag_block_horizontal : false,
  13354. drag_block_vertical : false,
  13355. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  13356. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  13357. drag_lock_to_axis : false,
  13358. // drag lock only kicks in when distance > drag_lock_min_distance
  13359. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  13360. drag_lock_min_distance : 25
  13361. },
  13362. triggered: false,
  13363. handler: function dragGesture(ev, inst) {
  13364. // current gesture isnt drag, but dragged is true
  13365. // this means an other gesture is busy. now call dragend
  13366. if(Hammer.detection.current.name != this.name && this.triggered) {
  13367. inst.trigger(this.name +'end', ev);
  13368. this.triggered = false;
  13369. return;
  13370. }
  13371. // max touches
  13372. if(inst.options.drag_max_touches > 0 &&
  13373. ev.touches.length > inst.options.drag_max_touches) {
  13374. return;
  13375. }
  13376. switch(ev.eventType) {
  13377. case Hammer.EVENT_START:
  13378. this.triggered = false;
  13379. break;
  13380. case Hammer.EVENT_MOVE:
  13381. // when the distance we moved is too small we skip this gesture
  13382. // or we can be already in dragging
  13383. if(ev.distance < inst.options.drag_min_distance &&
  13384. Hammer.detection.current.name != this.name) {
  13385. return;
  13386. }
  13387. // we are dragging!
  13388. Hammer.detection.current.name = this.name;
  13389. // lock drag to axis?
  13390. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  13391. ev.drag_locked_to_axis = true;
  13392. }
  13393. var last_direction = Hammer.detection.current.lastEvent.direction;
  13394. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  13395. // keep direction on the axis that the drag gesture started on
  13396. if(Hammer.utils.isVertical(last_direction)) {
  13397. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  13398. }
  13399. else {
  13400. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  13401. }
  13402. }
  13403. // first time, trigger dragstart event
  13404. if(!this.triggered) {
  13405. inst.trigger(this.name +'start', ev);
  13406. this.triggered = true;
  13407. }
  13408. // trigger normal event
  13409. inst.trigger(this.name, ev);
  13410. // direction event, like dragdown
  13411. inst.trigger(this.name + ev.direction, ev);
  13412. // block the browser events
  13413. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  13414. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  13415. ev.preventDefault();
  13416. }
  13417. break;
  13418. case Hammer.EVENT_END:
  13419. // trigger dragend
  13420. if(this.triggered) {
  13421. inst.trigger(this.name +'end', ev);
  13422. }
  13423. this.triggered = false;
  13424. break;
  13425. }
  13426. }
  13427. };
  13428. /**
  13429. * Transform
  13430. * User want to scale or rotate with 2 fingers
  13431. * @events transform, pinch, pinchin, pinchout, rotate
  13432. */
  13433. Hammer.gestures.Transform = {
  13434. name: 'transform',
  13435. index: 45,
  13436. defaults: {
  13437. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  13438. transform_min_scale : 0.01,
  13439. // rotation in degrees
  13440. transform_min_rotation : 1,
  13441. // prevent default browser behavior when two touches are on the screen
  13442. // but it makes the element a blocking element
  13443. // when you are using the transform gesture, it is a good practice to set this true
  13444. transform_always_block : false
  13445. },
  13446. triggered: false,
  13447. handler: function transformGesture(ev, inst) {
  13448. // current gesture isnt drag, but dragged is true
  13449. // this means an other gesture is busy. now call dragend
  13450. if(Hammer.detection.current.name != this.name && this.triggered) {
  13451. inst.trigger(this.name +'end', ev);
  13452. this.triggered = false;
  13453. return;
  13454. }
  13455. // atleast multitouch
  13456. if(ev.touches.length < 2) {
  13457. return;
  13458. }
  13459. // prevent default when two fingers are on the screen
  13460. if(inst.options.transform_always_block) {
  13461. ev.preventDefault();
  13462. }
  13463. switch(ev.eventType) {
  13464. case Hammer.EVENT_START:
  13465. this.triggered = false;
  13466. break;
  13467. case Hammer.EVENT_MOVE:
  13468. var scale_threshold = Math.abs(1-ev.scale);
  13469. var rotation_threshold = Math.abs(ev.rotation);
  13470. // when the distance we moved is too small we skip this gesture
  13471. // or we can be already in dragging
  13472. if(scale_threshold < inst.options.transform_min_scale &&
  13473. rotation_threshold < inst.options.transform_min_rotation) {
  13474. return;
  13475. }
  13476. // we are transforming!
  13477. Hammer.detection.current.name = this.name;
  13478. // first time, trigger dragstart event
  13479. if(!this.triggered) {
  13480. inst.trigger(this.name +'start', ev);
  13481. this.triggered = true;
  13482. }
  13483. inst.trigger(this.name, ev); // basic transform event
  13484. // trigger rotate event
  13485. if(rotation_threshold > inst.options.transform_min_rotation) {
  13486. inst.trigger('rotate', ev);
  13487. }
  13488. // trigger pinch event
  13489. if(scale_threshold > inst.options.transform_min_scale) {
  13490. inst.trigger('pinch', ev);
  13491. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  13492. }
  13493. break;
  13494. case Hammer.EVENT_END:
  13495. // trigger dragend
  13496. if(this.triggered) {
  13497. inst.trigger(this.name +'end', ev);
  13498. }
  13499. this.triggered = false;
  13500. break;
  13501. }
  13502. }
  13503. };
  13504. /**
  13505. * Touch
  13506. * Called as first, tells the user has touched the screen
  13507. * @events touch
  13508. */
  13509. Hammer.gestures.Touch = {
  13510. name: 'touch',
  13511. index: -Infinity,
  13512. defaults: {
  13513. // call preventDefault at touchstart, and makes the element blocking by
  13514. // disabling the scrolling of the page, but it improves gestures like
  13515. // transforming and dragging.
  13516. // be careful with using this, it can be very annoying for users to be stuck
  13517. // on the page
  13518. prevent_default: false,
  13519. // disable mouse events, so only touch (or pen!) input triggers events
  13520. prevent_mouseevents: false
  13521. },
  13522. handler: function touchGesture(ev, inst) {
  13523. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  13524. ev.stopDetect();
  13525. return;
  13526. }
  13527. if(inst.options.prevent_default) {
  13528. ev.preventDefault();
  13529. }
  13530. if(ev.eventType == Hammer.EVENT_START) {
  13531. inst.trigger(this.name, ev);
  13532. }
  13533. }
  13534. };
  13535. /**
  13536. * Release
  13537. * Called as last, tells the user has released the screen
  13538. * @events release
  13539. */
  13540. Hammer.gestures.Release = {
  13541. name: 'release',
  13542. index: Infinity,
  13543. handler: function releaseGesture(ev, inst) {
  13544. if(ev.eventType == Hammer.EVENT_END) {
  13545. inst.trigger(this.name, ev);
  13546. }
  13547. }
  13548. };
  13549. // node export
  13550. if(typeof module === 'object' && typeof module.exports === 'object'){
  13551. module.exports = Hammer;
  13552. }
  13553. // just window export
  13554. else {
  13555. window.Hammer = Hammer;
  13556. // requireJS module definition
  13557. if(typeof window.define === 'function' && window.define.amd) {
  13558. window.define('hammer', [], function() {
  13559. return Hammer;
  13560. });
  13561. }
  13562. }
  13563. })(this);
  13564. },{}],3:[function(require,module,exports){
  13565. //! moment.js
  13566. //! version : 2.5.0
  13567. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  13568. //! license : MIT
  13569. //! momentjs.com
  13570. (function (undefined) {
  13571. /************************************
  13572. Constants
  13573. ************************************/
  13574. var moment,
  13575. VERSION = "2.5.0",
  13576. global = this,
  13577. round = Math.round,
  13578. i,
  13579. YEAR = 0,
  13580. MONTH = 1,
  13581. DATE = 2,
  13582. HOUR = 3,
  13583. MINUTE = 4,
  13584. SECOND = 5,
  13585. MILLISECOND = 6,
  13586. // internal storage for language config files
  13587. languages = {},
  13588. // check for nodeJS
  13589. hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
  13590. // ASP.NET json date format regex
  13591. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  13592. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  13593. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  13594. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  13595. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  13596. // format tokens
  13597. 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,
  13598. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  13599. // parsing token regexes
  13600. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  13601. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  13602. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  13603. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  13604. parseTokenDigits = /\d+/, // nonzero number of digits
  13605. 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.
  13606. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  13607. parseTokenT = /T/i, // T (ISO separator)
  13608. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  13609. //strict parsing regexes
  13610. parseTokenOneDigit = /\d/, // 0 - 9
  13611. parseTokenTwoDigits = /\d\d/, // 00 - 99
  13612. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  13613. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  13614. parseTokenSixDigits = /[+\-]?\d{6}/, // -999,999 - 999,999
  13615. // iso 8601 regex
  13616. // 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)
  13617. isoRegex = /^\s*\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)?)?$/,
  13618. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  13619. isoDates = [
  13620. 'YYYY-MM-DD',
  13621. 'GGGG-[W]WW',
  13622. 'GGGG-[W]WW-E',
  13623. 'YYYY-DDD'
  13624. ],
  13625. // iso time formats and regexes
  13626. isoTimes = [
  13627. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  13628. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  13629. ['HH:mm', /(T| )\d\d:\d\d/],
  13630. ['HH', /(T| )\d\d/]
  13631. ],
  13632. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  13633. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  13634. // getter and setter names
  13635. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  13636. unitMillisecondFactors = {
  13637. 'Milliseconds' : 1,
  13638. 'Seconds' : 1e3,
  13639. 'Minutes' : 6e4,
  13640. 'Hours' : 36e5,
  13641. 'Days' : 864e5,
  13642. 'Months' : 2592e6,
  13643. 'Years' : 31536e6
  13644. },
  13645. unitAliases = {
  13646. ms : 'millisecond',
  13647. s : 'second',
  13648. m : 'minute',
  13649. h : 'hour',
  13650. d : 'day',
  13651. D : 'date',
  13652. w : 'week',
  13653. W : 'isoWeek',
  13654. M : 'month',
  13655. y : 'year',
  13656. DDD : 'dayOfYear',
  13657. e : 'weekday',
  13658. E : 'isoWeekday',
  13659. gg: 'weekYear',
  13660. GG: 'isoWeekYear'
  13661. },
  13662. camelFunctions = {
  13663. dayofyear : 'dayOfYear',
  13664. isoweekday : 'isoWeekday',
  13665. isoweek : 'isoWeek',
  13666. weekyear : 'weekYear',
  13667. isoweekyear : 'isoWeekYear'
  13668. },
  13669. // format function strings
  13670. formatFunctions = {},
  13671. // tokens to ordinalize and pad
  13672. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  13673. paddedTokens = 'M D H h m s w W'.split(' '),
  13674. formatTokenFunctions = {
  13675. M : function () {
  13676. return this.month() + 1;
  13677. },
  13678. MMM : function (format) {
  13679. return this.lang().monthsShort(this, format);
  13680. },
  13681. MMMM : function (format) {
  13682. return this.lang().months(this, format);
  13683. },
  13684. D : function () {
  13685. return this.date();
  13686. },
  13687. DDD : function () {
  13688. return this.dayOfYear();
  13689. },
  13690. d : function () {
  13691. return this.day();
  13692. },
  13693. dd : function (format) {
  13694. return this.lang().weekdaysMin(this, format);
  13695. },
  13696. ddd : function (format) {
  13697. return this.lang().weekdaysShort(this, format);
  13698. },
  13699. dddd : function (format) {
  13700. return this.lang().weekdays(this, format);
  13701. },
  13702. w : function () {
  13703. return this.week();
  13704. },
  13705. W : function () {
  13706. return this.isoWeek();
  13707. },
  13708. YY : function () {
  13709. return leftZeroFill(this.year() % 100, 2);
  13710. },
  13711. YYYY : function () {
  13712. return leftZeroFill(this.year(), 4);
  13713. },
  13714. YYYYY : function () {
  13715. return leftZeroFill(this.year(), 5);
  13716. },
  13717. YYYYYY : function () {
  13718. var y = this.year(), sign = y >= 0 ? '+' : '-';
  13719. return sign + leftZeroFill(Math.abs(y), 6);
  13720. },
  13721. gg : function () {
  13722. return leftZeroFill(this.weekYear() % 100, 2);
  13723. },
  13724. gggg : function () {
  13725. return this.weekYear();
  13726. },
  13727. ggggg : function () {
  13728. return leftZeroFill(this.weekYear(), 5);
  13729. },
  13730. GG : function () {
  13731. return leftZeroFill(this.isoWeekYear() % 100, 2);
  13732. },
  13733. GGGG : function () {
  13734. return this.isoWeekYear();
  13735. },
  13736. GGGGG : function () {
  13737. return leftZeroFill(this.isoWeekYear(), 5);
  13738. },
  13739. e : function () {
  13740. return this.weekday();
  13741. },
  13742. E : function () {
  13743. return this.isoWeekday();
  13744. },
  13745. a : function () {
  13746. return this.lang().meridiem(this.hours(), this.minutes(), true);
  13747. },
  13748. A : function () {
  13749. return this.lang().meridiem(this.hours(), this.minutes(), false);
  13750. },
  13751. H : function () {
  13752. return this.hours();
  13753. },
  13754. h : function () {
  13755. return this.hours() % 12 || 12;
  13756. },
  13757. m : function () {
  13758. return this.minutes();
  13759. },
  13760. s : function () {
  13761. return this.seconds();
  13762. },
  13763. S : function () {
  13764. return toInt(this.milliseconds() / 100);
  13765. },
  13766. SS : function () {
  13767. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  13768. },
  13769. SSS : function () {
  13770. return leftZeroFill(this.milliseconds(), 3);
  13771. },
  13772. SSSS : function () {
  13773. return leftZeroFill(this.milliseconds(), 3);
  13774. },
  13775. Z : function () {
  13776. var a = -this.zone(),
  13777. b = "+";
  13778. if (a < 0) {
  13779. a = -a;
  13780. b = "-";
  13781. }
  13782. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  13783. },
  13784. ZZ : function () {
  13785. var a = -this.zone(),
  13786. b = "+";
  13787. if (a < 0) {
  13788. a = -a;
  13789. b = "-";
  13790. }
  13791. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  13792. },
  13793. z : function () {
  13794. return this.zoneAbbr();
  13795. },
  13796. zz : function () {
  13797. return this.zoneName();
  13798. },
  13799. X : function () {
  13800. return this.unix();
  13801. },
  13802. Q : function () {
  13803. return this.quarter();
  13804. }
  13805. },
  13806. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  13807. function padToken(func, count) {
  13808. return function (a) {
  13809. return leftZeroFill(func.call(this, a), count);
  13810. };
  13811. }
  13812. function ordinalizeToken(func, period) {
  13813. return function (a) {
  13814. return this.lang().ordinal(func.call(this, a), period);
  13815. };
  13816. }
  13817. while (ordinalizeTokens.length) {
  13818. i = ordinalizeTokens.pop();
  13819. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  13820. }
  13821. while (paddedTokens.length) {
  13822. i = paddedTokens.pop();
  13823. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  13824. }
  13825. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  13826. /************************************
  13827. Constructors
  13828. ************************************/
  13829. function Language() {
  13830. }
  13831. // Moment prototype object
  13832. function Moment(config) {
  13833. checkOverflow(config);
  13834. extend(this, config);
  13835. }
  13836. // Duration Constructor
  13837. function Duration(duration) {
  13838. var normalizedInput = normalizeObjectUnits(duration),
  13839. years = normalizedInput.year || 0,
  13840. months = normalizedInput.month || 0,
  13841. weeks = normalizedInput.week || 0,
  13842. days = normalizedInput.day || 0,
  13843. hours = normalizedInput.hour || 0,
  13844. minutes = normalizedInput.minute || 0,
  13845. seconds = normalizedInput.second || 0,
  13846. milliseconds = normalizedInput.millisecond || 0;
  13847. // representation for dateAddRemove
  13848. this._milliseconds = +milliseconds +
  13849. seconds * 1e3 + // 1000
  13850. minutes * 6e4 + // 1000 * 60
  13851. hours * 36e5; // 1000 * 60 * 60
  13852. // Because of dateAddRemove treats 24 hours as different from a
  13853. // day when working around DST, we need to store them separately
  13854. this._days = +days +
  13855. weeks * 7;
  13856. // It is impossible translate months into days without knowing
  13857. // which months you are are talking about, so we have to store
  13858. // it separately.
  13859. this._months = +months +
  13860. years * 12;
  13861. this._data = {};
  13862. this._bubble();
  13863. }
  13864. /************************************
  13865. Helpers
  13866. ************************************/
  13867. function extend(a, b) {
  13868. for (var i in b) {
  13869. if (b.hasOwnProperty(i)) {
  13870. a[i] = b[i];
  13871. }
  13872. }
  13873. if (b.hasOwnProperty("toString")) {
  13874. a.toString = b.toString;
  13875. }
  13876. if (b.hasOwnProperty("valueOf")) {
  13877. a.valueOf = b.valueOf;
  13878. }
  13879. return a;
  13880. }
  13881. function absRound(number) {
  13882. if (number < 0) {
  13883. return Math.ceil(number);
  13884. } else {
  13885. return Math.floor(number);
  13886. }
  13887. }
  13888. // left zero fill a number
  13889. // see http://jsperf.com/left-zero-filling for performance comparison
  13890. function leftZeroFill(number, targetLength, forceSign) {
  13891. var output = Math.abs(number) + '',
  13892. sign = number >= 0;
  13893. while (output.length < targetLength) {
  13894. output = '0' + output;
  13895. }
  13896. return (sign ? (forceSign ? '+' : '') : '-') + output;
  13897. }
  13898. // helper function for _.addTime and _.subtractTime
  13899. function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
  13900. var milliseconds = duration._milliseconds,
  13901. days = duration._days,
  13902. months = duration._months,
  13903. minutes,
  13904. hours;
  13905. if (milliseconds) {
  13906. mom._d.setTime(+mom._d + milliseconds * isAdding);
  13907. }
  13908. // store the minutes and hours so we can restore them
  13909. if (days || months) {
  13910. minutes = mom.minute();
  13911. hours = mom.hour();
  13912. }
  13913. if (days) {
  13914. mom.date(mom.date() + days * isAdding);
  13915. }
  13916. if (months) {
  13917. mom.month(mom.month() + months * isAdding);
  13918. }
  13919. if (milliseconds && !ignoreUpdateOffset) {
  13920. moment.updateOffset(mom);
  13921. }
  13922. // restore the minutes and hours after possibly changing dst
  13923. if (days || months) {
  13924. mom.minute(minutes);
  13925. mom.hour(hours);
  13926. }
  13927. }
  13928. // check if is an array
  13929. function isArray(input) {
  13930. return Object.prototype.toString.call(input) === '[object Array]';
  13931. }
  13932. function isDate(input) {
  13933. return Object.prototype.toString.call(input) === '[object Date]' ||
  13934. input instanceof Date;
  13935. }
  13936. // compare two arrays, return the number of differences
  13937. function compareArrays(array1, array2, dontConvert) {
  13938. var len = Math.min(array1.length, array2.length),
  13939. lengthDiff = Math.abs(array1.length - array2.length),
  13940. diffs = 0,
  13941. i;
  13942. for (i = 0; i < len; i++) {
  13943. if ((dontConvert && array1[i] !== array2[i]) ||
  13944. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  13945. diffs++;
  13946. }
  13947. }
  13948. return diffs + lengthDiff;
  13949. }
  13950. function normalizeUnits(units) {
  13951. if (units) {
  13952. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  13953. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  13954. }
  13955. return units;
  13956. }
  13957. function normalizeObjectUnits(inputObject) {
  13958. var normalizedInput = {},
  13959. normalizedProp,
  13960. prop;
  13961. for (prop in inputObject) {
  13962. if (inputObject.hasOwnProperty(prop)) {
  13963. normalizedProp = normalizeUnits(prop);
  13964. if (normalizedProp) {
  13965. normalizedInput[normalizedProp] = inputObject[prop];
  13966. }
  13967. }
  13968. }
  13969. return normalizedInput;
  13970. }
  13971. function makeList(field) {
  13972. var count, setter;
  13973. if (field.indexOf('week') === 0) {
  13974. count = 7;
  13975. setter = 'day';
  13976. }
  13977. else if (field.indexOf('month') === 0) {
  13978. count = 12;
  13979. setter = 'month';
  13980. }
  13981. else {
  13982. return;
  13983. }
  13984. moment[field] = function (format, index) {
  13985. var i, getter,
  13986. method = moment.fn._lang[field],
  13987. results = [];
  13988. if (typeof format === 'number') {
  13989. index = format;
  13990. format = undefined;
  13991. }
  13992. getter = function (i) {
  13993. var m = moment().utc().set(setter, i);
  13994. return method.call(moment.fn._lang, m, format || '');
  13995. };
  13996. if (index != null) {
  13997. return getter(index);
  13998. }
  13999. else {
  14000. for (i = 0; i < count; i++) {
  14001. results.push(getter(i));
  14002. }
  14003. return results;
  14004. }
  14005. };
  14006. }
  14007. function toInt(argumentForCoercion) {
  14008. var coercedNumber = +argumentForCoercion,
  14009. value = 0;
  14010. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  14011. if (coercedNumber >= 0) {
  14012. value = Math.floor(coercedNumber);
  14013. } else {
  14014. value = Math.ceil(coercedNumber);
  14015. }
  14016. }
  14017. return value;
  14018. }
  14019. function daysInMonth(year, month) {
  14020. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  14021. }
  14022. function daysInYear(year) {
  14023. return isLeapYear(year) ? 366 : 365;
  14024. }
  14025. function isLeapYear(year) {
  14026. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  14027. }
  14028. function checkOverflow(m) {
  14029. var overflow;
  14030. if (m._a && m._pf.overflow === -2) {
  14031. overflow =
  14032. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  14033. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  14034. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  14035. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  14036. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  14037. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  14038. -1;
  14039. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  14040. overflow = DATE;
  14041. }
  14042. m._pf.overflow = overflow;
  14043. }
  14044. }
  14045. function initializeParsingFlags(config) {
  14046. config._pf = {
  14047. empty : false,
  14048. unusedTokens : [],
  14049. unusedInput : [],
  14050. overflow : -2,
  14051. charsLeftOver : 0,
  14052. nullInput : false,
  14053. invalidMonth : null,
  14054. invalidFormat : false,
  14055. userInvalidated : false,
  14056. iso: false
  14057. };
  14058. }
  14059. function isValid(m) {
  14060. if (m._isValid == null) {
  14061. m._isValid = !isNaN(m._d.getTime()) &&
  14062. m._pf.overflow < 0 &&
  14063. !m._pf.empty &&
  14064. !m._pf.invalidMonth &&
  14065. !m._pf.nullInput &&
  14066. !m._pf.invalidFormat &&
  14067. !m._pf.userInvalidated;
  14068. if (m._strict) {
  14069. m._isValid = m._isValid &&
  14070. m._pf.charsLeftOver === 0 &&
  14071. m._pf.unusedTokens.length === 0;
  14072. }
  14073. }
  14074. return m._isValid;
  14075. }
  14076. function normalizeLanguage(key) {
  14077. return key ? key.toLowerCase().replace('_', '-') : key;
  14078. }
  14079. // Return a moment from input, that is local/utc/zone equivalent to model.
  14080. function makeAs(input, model) {
  14081. return model._isUTC ? moment(input).zone(model._offset || 0) :
  14082. moment(input).local();
  14083. }
  14084. /************************************
  14085. Languages
  14086. ************************************/
  14087. extend(Language.prototype, {
  14088. set : function (config) {
  14089. var prop, i;
  14090. for (i in config) {
  14091. prop = config[i];
  14092. if (typeof prop === 'function') {
  14093. this[i] = prop;
  14094. } else {
  14095. this['_' + i] = prop;
  14096. }
  14097. }
  14098. },
  14099. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  14100. months : function (m) {
  14101. return this._months[m.month()];
  14102. },
  14103. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  14104. monthsShort : function (m) {
  14105. return this._monthsShort[m.month()];
  14106. },
  14107. monthsParse : function (monthName) {
  14108. var i, mom, regex;
  14109. if (!this._monthsParse) {
  14110. this._monthsParse = [];
  14111. }
  14112. for (i = 0; i < 12; i++) {
  14113. // make the regex if we don't have it already
  14114. if (!this._monthsParse[i]) {
  14115. mom = moment.utc([2000, i]);
  14116. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  14117. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  14118. }
  14119. // test the regex
  14120. if (this._monthsParse[i].test(monthName)) {
  14121. return i;
  14122. }
  14123. }
  14124. },
  14125. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  14126. weekdays : function (m) {
  14127. return this._weekdays[m.day()];
  14128. },
  14129. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  14130. weekdaysShort : function (m) {
  14131. return this._weekdaysShort[m.day()];
  14132. },
  14133. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  14134. weekdaysMin : function (m) {
  14135. return this._weekdaysMin[m.day()];
  14136. },
  14137. weekdaysParse : function (weekdayName) {
  14138. var i, mom, regex;
  14139. if (!this._weekdaysParse) {
  14140. this._weekdaysParse = [];
  14141. }
  14142. for (i = 0; i < 7; i++) {
  14143. // make the regex if we don't have it already
  14144. if (!this._weekdaysParse[i]) {
  14145. mom = moment([2000, 1]).day(i);
  14146. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  14147. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  14148. }
  14149. // test the regex
  14150. if (this._weekdaysParse[i].test(weekdayName)) {
  14151. return i;
  14152. }
  14153. }
  14154. },
  14155. _longDateFormat : {
  14156. LT : "h:mm A",
  14157. L : "MM/DD/YYYY",
  14158. LL : "MMMM D YYYY",
  14159. LLL : "MMMM D YYYY LT",
  14160. LLLL : "dddd, MMMM D YYYY LT"
  14161. },
  14162. longDateFormat : function (key) {
  14163. var output = this._longDateFormat[key];
  14164. if (!output && this._longDateFormat[key.toUpperCase()]) {
  14165. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  14166. return val.slice(1);
  14167. });
  14168. this._longDateFormat[key] = output;
  14169. }
  14170. return output;
  14171. },
  14172. isPM : function (input) {
  14173. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  14174. // Using charAt should be more compatible.
  14175. return ((input + '').toLowerCase().charAt(0) === 'p');
  14176. },
  14177. _meridiemParse : /[ap]\.?m?\.?/i,
  14178. meridiem : function (hours, minutes, isLower) {
  14179. if (hours > 11) {
  14180. return isLower ? 'pm' : 'PM';
  14181. } else {
  14182. return isLower ? 'am' : 'AM';
  14183. }
  14184. },
  14185. _calendar : {
  14186. sameDay : '[Today at] LT',
  14187. nextDay : '[Tomorrow at] LT',
  14188. nextWeek : 'dddd [at] LT',
  14189. lastDay : '[Yesterday at] LT',
  14190. lastWeek : '[Last] dddd [at] LT',
  14191. sameElse : 'L'
  14192. },
  14193. calendar : function (key, mom) {
  14194. var output = this._calendar[key];
  14195. return typeof output === 'function' ? output.apply(mom) : output;
  14196. },
  14197. _relativeTime : {
  14198. future : "in %s",
  14199. past : "%s ago",
  14200. s : "a few seconds",
  14201. m : "a minute",
  14202. mm : "%d minutes",
  14203. h : "an hour",
  14204. hh : "%d hours",
  14205. d : "a day",
  14206. dd : "%d days",
  14207. M : "a month",
  14208. MM : "%d months",
  14209. y : "a year",
  14210. yy : "%d years"
  14211. },
  14212. relativeTime : function (number, withoutSuffix, string, isFuture) {
  14213. var output = this._relativeTime[string];
  14214. return (typeof output === 'function') ?
  14215. output(number, withoutSuffix, string, isFuture) :
  14216. output.replace(/%d/i, number);
  14217. },
  14218. pastFuture : function (diff, output) {
  14219. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  14220. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  14221. },
  14222. ordinal : function (number) {
  14223. return this._ordinal.replace("%d", number);
  14224. },
  14225. _ordinal : "%d",
  14226. preparse : function (string) {
  14227. return string;
  14228. },
  14229. postformat : function (string) {
  14230. return string;
  14231. },
  14232. week : function (mom) {
  14233. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  14234. },
  14235. _week : {
  14236. dow : 0, // Sunday is the first day of the week.
  14237. doy : 6 // The week that contains Jan 1st is the first week of the year.
  14238. },
  14239. _invalidDate: 'Invalid date',
  14240. invalidDate: function () {
  14241. return this._invalidDate;
  14242. }
  14243. });
  14244. // Loads a language definition into the `languages` cache. The function
  14245. // takes a key and optionally values. If not in the browser and no values
  14246. // are provided, it will load the language file module. As a convenience,
  14247. // this function also returns the language values.
  14248. function loadLang(key, values) {
  14249. values.abbr = key;
  14250. if (!languages[key]) {
  14251. languages[key] = new Language();
  14252. }
  14253. languages[key].set(values);
  14254. return languages[key];
  14255. }
  14256. // Remove a language from the `languages` cache. Mostly useful in tests.
  14257. function unloadLang(key) {
  14258. delete languages[key];
  14259. }
  14260. // Determines which language definition to use and returns it.
  14261. //
  14262. // With no parameters, it will return the global language. If you
  14263. // pass in a language key, such as 'en', it will return the
  14264. // definition for 'en', so long as 'en' has already been loaded using
  14265. // moment.lang.
  14266. function getLangDefinition(key) {
  14267. var i = 0, j, lang, next, split,
  14268. get = function (k) {
  14269. if (!languages[k] && hasModule) {
  14270. try {
  14271. require('./lang/' + k);
  14272. } catch (e) { }
  14273. }
  14274. return languages[k];
  14275. };
  14276. if (!key) {
  14277. return moment.fn._lang;
  14278. }
  14279. if (!isArray(key)) {
  14280. //short-circuit everything else
  14281. lang = get(key);
  14282. if (lang) {
  14283. return lang;
  14284. }
  14285. key = [key];
  14286. }
  14287. //pick the language from the array
  14288. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  14289. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  14290. while (i < key.length) {
  14291. split = normalizeLanguage(key[i]).split('-');
  14292. j = split.length;
  14293. next = normalizeLanguage(key[i + 1]);
  14294. next = next ? next.split('-') : null;
  14295. while (j > 0) {
  14296. lang = get(split.slice(0, j).join('-'));
  14297. if (lang) {
  14298. return lang;
  14299. }
  14300. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  14301. //the next array item is better than a shallower substring of this one
  14302. break;
  14303. }
  14304. j--;
  14305. }
  14306. i++;
  14307. }
  14308. return moment.fn._lang;
  14309. }
  14310. /************************************
  14311. Formatting
  14312. ************************************/
  14313. function removeFormattingTokens(input) {
  14314. if (input.match(/\[[\s\S]/)) {
  14315. return input.replace(/^\[|\]$/g, "");
  14316. }
  14317. return input.replace(/\\/g, "");
  14318. }
  14319. function makeFormatFunction(format) {
  14320. var array = format.match(formattingTokens), i, length;
  14321. for (i = 0, length = array.length; i < length; i++) {
  14322. if (formatTokenFunctions[array[i]]) {
  14323. array[i] = formatTokenFunctions[array[i]];
  14324. } else {
  14325. array[i] = removeFormattingTokens(array[i]);
  14326. }
  14327. }
  14328. return function (mom) {
  14329. var output = "";
  14330. for (i = 0; i < length; i++) {
  14331. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  14332. }
  14333. return output;
  14334. };
  14335. }
  14336. // format date using native date object
  14337. function formatMoment(m, format) {
  14338. if (!m.isValid()) {
  14339. return m.lang().invalidDate();
  14340. }
  14341. format = expandFormat(format, m.lang());
  14342. if (!formatFunctions[format]) {
  14343. formatFunctions[format] = makeFormatFunction(format);
  14344. }
  14345. return formatFunctions[format](m);
  14346. }
  14347. function expandFormat(format, lang) {
  14348. var i = 5;
  14349. function replaceLongDateFormatTokens(input) {
  14350. return lang.longDateFormat(input) || input;
  14351. }
  14352. localFormattingTokens.lastIndex = 0;
  14353. while (i >= 0 && localFormattingTokens.test(format)) {
  14354. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  14355. localFormattingTokens.lastIndex = 0;
  14356. i -= 1;
  14357. }
  14358. return format;
  14359. }
  14360. /************************************
  14361. Parsing
  14362. ************************************/
  14363. // get the regex to find the next token
  14364. function getParseRegexForToken(token, config) {
  14365. var a, strict = config._strict;
  14366. switch (token) {
  14367. case 'DDDD':
  14368. return parseTokenThreeDigits;
  14369. case 'YYYY':
  14370. case 'GGGG':
  14371. case 'gggg':
  14372. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  14373. case 'YYYYYY':
  14374. case 'YYYYY':
  14375. case 'GGGGG':
  14376. case 'ggggg':
  14377. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  14378. case 'S':
  14379. if (strict) { return parseTokenOneDigit; }
  14380. /* falls through */
  14381. case 'SS':
  14382. if (strict) { return parseTokenTwoDigits; }
  14383. /* falls through */
  14384. case 'SSS':
  14385. case 'DDD':
  14386. return strict ? parseTokenThreeDigits : parseTokenOneToThreeDigits;
  14387. case 'MMM':
  14388. case 'MMMM':
  14389. case 'dd':
  14390. case 'ddd':
  14391. case 'dddd':
  14392. return parseTokenWord;
  14393. case 'a':
  14394. case 'A':
  14395. return getLangDefinition(config._l)._meridiemParse;
  14396. case 'X':
  14397. return parseTokenTimestampMs;
  14398. case 'Z':
  14399. case 'ZZ':
  14400. return parseTokenTimezone;
  14401. case 'T':
  14402. return parseTokenT;
  14403. case 'SSSS':
  14404. return parseTokenDigits;
  14405. case 'MM':
  14406. case 'DD':
  14407. case 'YY':
  14408. case 'GG':
  14409. case 'gg':
  14410. case 'HH':
  14411. case 'hh':
  14412. case 'mm':
  14413. case 'ss':
  14414. case 'ww':
  14415. case 'WW':
  14416. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  14417. case 'M':
  14418. case 'D':
  14419. case 'd':
  14420. case 'H':
  14421. case 'h':
  14422. case 'm':
  14423. case 's':
  14424. case 'w':
  14425. case 'W':
  14426. case 'e':
  14427. case 'E':
  14428. return strict ? parseTokenOneDigit : parseTokenOneOrTwoDigits;
  14429. default :
  14430. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  14431. return a;
  14432. }
  14433. }
  14434. function timezoneMinutesFromString(string) {
  14435. string = string || "";
  14436. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  14437. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  14438. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  14439. minutes = +(parts[1] * 60) + toInt(parts[2]);
  14440. return parts[0] === '+' ? -minutes : minutes;
  14441. }
  14442. // function to convert string input to date
  14443. function addTimeToArrayFromToken(token, input, config) {
  14444. var a, datePartArray = config._a;
  14445. switch (token) {
  14446. // MONTH
  14447. case 'M' : // fall through to MM
  14448. case 'MM' :
  14449. if (input != null) {
  14450. datePartArray[MONTH] = toInt(input) - 1;
  14451. }
  14452. break;
  14453. case 'MMM' : // fall through to MMMM
  14454. case 'MMMM' :
  14455. a = getLangDefinition(config._l).monthsParse(input);
  14456. // if we didn't find a month name, mark the date as invalid.
  14457. if (a != null) {
  14458. datePartArray[MONTH] = a;
  14459. } else {
  14460. config._pf.invalidMonth = input;
  14461. }
  14462. break;
  14463. // DAY OF MONTH
  14464. case 'D' : // fall through to DD
  14465. case 'DD' :
  14466. if (input != null) {
  14467. datePartArray[DATE] = toInt(input);
  14468. }
  14469. break;
  14470. // DAY OF YEAR
  14471. case 'DDD' : // fall through to DDDD
  14472. case 'DDDD' :
  14473. if (input != null) {
  14474. config._dayOfYear = toInt(input);
  14475. }
  14476. break;
  14477. // YEAR
  14478. case 'YY' :
  14479. datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  14480. break;
  14481. case 'YYYY' :
  14482. case 'YYYYY' :
  14483. case 'YYYYYY' :
  14484. datePartArray[YEAR] = toInt(input);
  14485. break;
  14486. // AM / PM
  14487. case 'a' : // fall through to A
  14488. case 'A' :
  14489. config._isPm = getLangDefinition(config._l).isPM(input);
  14490. break;
  14491. // 24 HOUR
  14492. case 'H' : // fall through to hh
  14493. case 'HH' : // fall through to hh
  14494. case 'h' : // fall through to hh
  14495. case 'hh' :
  14496. datePartArray[HOUR] = toInt(input);
  14497. break;
  14498. // MINUTE
  14499. case 'm' : // fall through to mm
  14500. case 'mm' :
  14501. datePartArray[MINUTE] = toInt(input);
  14502. break;
  14503. // SECOND
  14504. case 's' : // fall through to ss
  14505. case 'ss' :
  14506. datePartArray[SECOND] = toInt(input);
  14507. break;
  14508. // MILLISECOND
  14509. case 'S' :
  14510. case 'SS' :
  14511. case 'SSS' :
  14512. case 'SSSS' :
  14513. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  14514. break;
  14515. // UNIX TIMESTAMP WITH MS
  14516. case 'X':
  14517. config._d = new Date(parseFloat(input) * 1000);
  14518. break;
  14519. // TIMEZONE
  14520. case 'Z' : // fall through to ZZ
  14521. case 'ZZ' :
  14522. config._useUTC = true;
  14523. config._tzm = timezoneMinutesFromString(input);
  14524. break;
  14525. case 'w':
  14526. case 'ww':
  14527. case 'W':
  14528. case 'WW':
  14529. case 'd':
  14530. case 'dd':
  14531. case 'ddd':
  14532. case 'dddd':
  14533. case 'e':
  14534. case 'E':
  14535. token = token.substr(0, 1);
  14536. /* falls through */
  14537. case 'gg':
  14538. case 'gggg':
  14539. case 'GG':
  14540. case 'GGGG':
  14541. case 'GGGGG':
  14542. token = token.substr(0, 2);
  14543. if (input) {
  14544. config._w = config._w || {};
  14545. config._w[token] = input;
  14546. }
  14547. break;
  14548. }
  14549. }
  14550. // convert an array to a date.
  14551. // the array should mirror the parameters below
  14552. // note: all values past the year are optional and will default to the lowest possible value.
  14553. // [year, month, day , hour, minute, second, millisecond]
  14554. function dateFromConfig(config) {
  14555. var i, date, input = [], currentDate,
  14556. yearToUse, fixYear, w, temp, lang, weekday, week;
  14557. if (config._d) {
  14558. return;
  14559. }
  14560. currentDate = currentDateArray(config);
  14561. //compute day of the year from weeks and weekdays
  14562. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  14563. fixYear = function (val) {
  14564. var int_val = parseInt(val, 10);
  14565. return val ?
  14566. (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
  14567. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  14568. };
  14569. w = config._w;
  14570. if (w.GG != null || w.W != null || w.E != null) {
  14571. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  14572. }
  14573. else {
  14574. lang = getLangDefinition(config._l);
  14575. weekday = w.d != null ? parseWeekday(w.d, lang) :
  14576. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  14577. week = parseInt(w.w, 10) || 1;
  14578. //if we're parsing 'd', then the low day numbers may be next week
  14579. if (w.d != null && weekday < lang._week.dow) {
  14580. week++;
  14581. }
  14582. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  14583. }
  14584. config._a[YEAR] = temp.year;
  14585. config._dayOfYear = temp.dayOfYear;
  14586. }
  14587. //if the day of the year is set, figure out what it is
  14588. if (config._dayOfYear) {
  14589. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  14590. if (config._dayOfYear > daysInYear(yearToUse)) {
  14591. config._pf._overflowDayOfYear = true;
  14592. }
  14593. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  14594. config._a[MONTH] = date.getUTCMonth();
  14595. config._a[DATE] = date.getUTCDate();
  14596. }
  14597. // Default to current date.
  14598. // * if no year, month, day of month are given, default to today
  14599. // * if day of month is given, default month and year
  14600. // * if month is given, default only year
  14601. // * if year is given, don't default anything
  14602. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  14603. config._a[i] = input[i] = currentDate[i];
  14604. }
  14605. // Zero out whatever was not defaulted, including time
  14606. for (; i < 7; i++) {
  14607. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  14608. }
  14609. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  14610. input[HOUR] += toInt((config._tzm || 0) / 60);
  14611. input[MINUTE] += toInt((config._tzm || 0) % 60);
  14612. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  14613. }
  14614. function dateFromObject(config) {
  14615. var normalizedInput;
  14616. if (config._d) {
  14617. return;
  14618. }
  14619. normalizedInput = normalizeObjectUnits(config._i);
  14620. config._a = [
  14621. normalizedInput.year,
  14622. normalizedInput.month,
  14623. normalizedInput.day,
  14624. normalizedInput.hour,
  14625. normalizedInput.minute,
  14626. normalizedInput.second,
  14627. normalizedInput.millisecond
  14628. ];
  14629. dateFromConfig(config);
  14630. }
  14631. function currentDateArray(config) {
  14632. var now = new Date();
  14633. if (config._useUTC) {
  14634. return [
  14635. now.getUTCFullYear(),
  14636. now.getUTCMonth(),
  14637. now.getUTCDate()
  14638. ];
  14639. } else {
  14640. return [now.getFullYear(), now.getMonth(), now.getDate()];
  14641. }
  14642. }
  14643. // date from string and format string
  14644. function makeDateFromStringAndFormat(config) {
  14645. config._a = [];
  14646. config._pf.empty = true;
  14647. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  14648. var lang = getLangDefinition(config._l),
  14649. string = '' + config._i,
  14650. i, parsedInput, tokens, token, skipped,
  14651. stringLength = string.length,
  14652. totalParsedInputLength = 0;
  14653. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  14654. for (i = 0; i < tokens.length; i++) {
  14655. token = tokens[i];
  14656. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  14657. if (parsedInput) {
  14658. skipped = string.substr(0, string.indexOf(parsedInput));
  14659. if (skipped.length > 0) {
  14660. config._pf.unusedInput.push(skipped);
  14661. }
  14662. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  14663. totalParsedInputLength += parsedInput.length;
  14664. }
  14665. // don't parse if it's not a known token
  14666. if (formatTokenFunctions[token]) {
  14667. if (parsedInput) {
  14668. config._pf.empty = false;
  14669. }
  14670. else {
  14671. config._pf.unusedTokens.push(token);
  14672. }
  14673. addTimeToArrayFromToken(token, parsedInput, config);
  14674. }
  14675. else if (config._strict && !parsedInput) {
  14676. config._pf.unusedTokens.push(token);
  14677. }
  14678. }
  14679. // add remaining unparsed input length to the string
  14680. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  14681. if (string.length > 0) {
  14682. config._pf.unusedInput.push(string);
  14683. }
  14684. // handle am pm
  14685. if (config._isPm && config._a[HOUR] < 12) {
  14686. config._a[HOUR] += 12;
  14687. }
  14688. // if is 12 am, change hours to 0
  14689. if (config._isPm === false && config._a[HOUR] === 12) {
  14690. config._a[HOUR] = 0;
  14691. }
  14692. dateFromConfig(config);
  14693. checkOverflow(config);
  14694. }
  14695. function unescapeFormat(s) {
  14696. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  14697. return p1 || p2 || p3 || p4;
  14698. });
  14699. }
  14700. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  14701. function regexpEscape(s) {
  14702. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  14703. }
  14704. // date from string and array of format strings
  14705. function makeDateFromStringAndArray(config) {
  14706. var tempConfig,
  14707. bestMoment,
  14708. scoreToBeat,
  14709. i,
  14710. currentScore;
  14711. if (config._f.length === 0) {
  14712. config._pf.invalidFormat = true;
  14713. config._d = new Date(NaN);
  14714. return;
  14715. }
  14716. for (i = 0; i < config._f.length; i++) {
  14717. currentScore = 0;
  14718. tempConfig = extend({}, config);
  14719. initializeParsingFlags(tempConfig);
  14720. tempConfig._f = config._f[i];
  14721. makeDateFromStringAndFormat(tempConfig);
  14722. if (!isValid(tempConfig)) {
  14723. continue;
  14724. }
  14725. // if there is any input that was not parsed add a penalty for that format
  14726. currentScore += tempConfig._pf.charsLeftOver;
  14727. //or tokens
  14728. currentScore += tempConfig._pf.unusedTokens.length * 10;
  14729. tempConfig._pf.score = currentScore;
  14730. if (scoreToBeat == null || currentScore < scoreToBeat) {
  14731. scoreToBeat = currentScore;
  14732. bestMoment = tempConfig;
  14733. }
  14734. }
  14735. extend(config, bestMoment || tempConfig);
  14736. }
  14737. // date from iso format
  14738. function makeDateFromString(config) {
  14739. var i,
  14740. string = config._i,
  14741. match = isoRegex.exec(string);
  14742. if (match) {
  14743. config._pf.iso = true;
  14744. for (i = 4; i > 0; i--) {
  14745. if (match[i]) {
  14746. // match[5] should be "T" or undefined
  14747. config._f = isoDates[i - 1] + (match[6] || " ");
  14748. break;
  14749. }
  14750. }
  14751. for (i = 0; i < 4; i++) {
  14752. if (isoTimes[i][1].exec(string)) {
  14753. config._f += isoTimes[i][0];
  14754. break;
  14755. }
  14756. }
  14757. if (string.match(parseTokenTimezone)) {
  14758. config._f += "Z";
  14759. }
  14760. makeDateFromStringAndFormat(config);
  14761. }
  14762. else {
  14763. config._d = new Date(string);
  14764. }
  14765. }
  14766. function makeDateFromInput(config) {
  14767. var input = config._i,
  14768. matched = aspNetJsonRegex.exec(input);
  14769. if (input === undefined) {
  14770. config._d = new Date();
  14771. } else if (matched) {
  14772. config._d = new Date(+matched[1]);
  14773. } else if (typeof input === 'string') {
  14774. makeDateFromString(config);
  14775. } else if (isArray(input)) {
  14776. config._a = input.slice(0);
  14777. dateFromConfig(config);
  14778. } else if (isDate(input)) {
  14779. config._d = new Date(+input);
  14780. } else if (typeof(input) === 'object') {
  14781. dateFromObject(config);
  14782. } else {
  14783. config._d = new Date(input);
  14784. }
  14785. }
  14786. function makeDate(y, m, d, h, M, s, ms) {
  14787. //can't just apply() to create a date:
  14788. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  14789. var date = new Date(y, m, d, h, M, s, ms);
  14790. //the date constructor doesn't accept years < 1970
  14791. if (y < 1970) {
  14792. date.setFullYear(y);
  14793. }
  14794. return date;
  14795. }
  14796. function makeUTCDate(y) {
  14797. var date = new Date(Date.UTC.apply(null, arguments));
  14798. if (y < 1970) {
  14799. date.setUTCFullYear(y);
  14800. }
  14801. return date;
  14802. }
  14803. function parseWeekday(input, language) {
  14804. if (typeof input === 'string') {
  14805. if (!isNaN(input)) {
  14806. input = parseInt(input, 10);
  14807. }
  14808. else {
  14809. input = language.weekdaysParse(input);
  14810. if (typeof input !== 'number') {
  14811. return null;
  14812. }
  14813. }
  14814. }
  14815. return input;
  14816. }
  14817. /************************************
  14818. Relative Time
  14819. ************************************/
  14820. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  14821. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  14822. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  14823. }
  14824. function relativeTime(milliseconds, withoutSuffix, lang) {
  14825. var seconds = round(Math.abs(milliseconds) / 1000),
  14826. minutes = round(seconds / 60),
  14827. hours = round(minutes / 60),
  14828. days = round(hours / 24),
  14829. years = round(days / 365),
  14830. args = seconds < 45 && ['s', seconds] ||
  14831. minutes === 1 && ['m'] ||
  14832. minutes < 45 && ['mm', minutes] ||
  14833. hours === 1 && ['h'] ||
  14834. hours < 22 && ['hh', hours] ||
  14835. days === 1 && ['d'] ||
  14836. days <= 25 && ['dd', days] ||
  14837. days <= 45 && ['M'] ||
  14838. days < 345 && ['MM', round(days / 30)] ||
  14839. years === 1 && ['y'] || ['yy', years];
  14840. args[2] = withoutSuffix;
  14841. args[3] = milliseconds > 0;
  14842. args[4] = lang;
  14843. return substituteTimeAgo.apply({}, args);
  14844. }
  14845. /************************************
  14846. Week of Year
  14847. ************************************/
  14848. // firstDayOfWeek 0 = sun, 6 = sat
  14849. // the day of the week that starts the week
  14850. // (usually sunday or monday)
  14851. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  14852. // the first week is the week that contains the first
  14853. // of this day of the week
  14854. // (eg. ISO weeks use thursday (4))
  14855. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  14856. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  14857. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  14858. adjustedMoment;
  14859. if (daysToDayOfWeek > end) {
  14860. daysToDayOfWeek -= 7;
  14861. }
  14862. if (daysToDayOfWeek < end - 7) {
  14863. daysToDayOfWeek += 7;
  14864. }
  14865. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  14866. return {
  14867. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  14868. year: adjustedMoment.year()
  14869. };
  14870. }
  14871. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  14872. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  14873. // The only solid way to create an iso date from year is to use
  14874. // a string format (Date.UTC handles only years > 1900). Don't ask why
  14875. // it doesn't need Z at the end.
  14876. var d = new Date(leftZeroFill(year, 6, true) + '-01-01').getUTCDay(),
  14877. daysToAdd, dayOfYear;
  14878. weekday = weekday != null ? weekday : firstDayOfWeek;
  14879. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0);
  14880. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  14881. return {
  14882. year: dayOfYear > 0 ? year : year - 1,
  14883. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  14884. };
  14885. }
  14886. /************************************
  14887. Top Level Functions
  14888. ************************************/
  14889. function makeMoment(config) {
  14890. var input = config._i,
  14891. format = config._f;
  14892. if (typeof config._pf === 'undefined') {
  14893. initializeParsingFlags(config);
  14894. }
  14895. if (input === null) {
  14896. return moment.invalid({nullInput: true});
  14897. }
  14898. if (typeof input === 'string') {
  14899. config._i = input = getLangDefinition().preparse(input);
  14900. }
  14901. if (moment.isMoment(input)) {
  14902. config = extend({}, input);
  14903. config._d = new Date(+input._d);
  14904. } else if (format) {
  14905. if (isArray(format)) {
  14906. makeDateFromStringAndArray(config);
  14907. } else {
  14908. makeDateFromStringAndFormat(config);
  14909. }
  14910. } else {
  14911. makeDateFromInput(config);
  14912. }
  14913. return new Moment(config);
  14914. }
  14915. moment = function (input, format, lang, strict) {
  14916. if (typeof(lang) === "boolean") {
  14917. strict = lang;
  14918. lang = undefined;
  14919. }
  14920. return makeMoment({
  14921. _i : input,
  14922. _f : format,
  14923. _l : lang,
  14924. _strict : strict,
  14925. _isUTC : false
  14926. });
  14927. };
  14928. // creating with utc
  14929. moment.utc = function (input, format, lang, strict) {
  14930. var m;
  14931. if (typeof(lang) === "boolean") {
  14932. strict = lang;
  14933. lang = undefined;
  14934. }
  14935. m = makeMoment({
  14936. _useUTC : true,
  14937. _isUTC : true,
  14938. _l : lang,
  14939. _i : input,
  14940. _f : format,
  14941. _strict : strict
  14942. }).utc();
  14943. return m;
  14944. };
  14945. // creating with unix timestamp (in seconds)
  14946. moment.unix = function (input) {
  14947. return moment(input * 1000);
  14948. };
  14949. // duration
  14950. moment.duration = function (input, key) {
  14951. var duration = input,
  14952. // matching against regexp is expensive, do it on demand
  14953. match = null,
  14954. sign,
  14955. ret,
  14956. parseIso;
  14957. if (moment.isDuration(input)) {
  14958. duration = {
  14959. ms: input._milliseconds,
  14960. d: input._days,
  14961. M: input._months
  14962. };
  14963. } else if (typeof input === 'number') {
  14964. duration = {};
  14965. if (key) {
  14966. duration[key] = input;
  14967. } else {
  14968. duration.milliseconds = input;
  14969. }
  14970. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  14971. sign = (match[1] === "-") ? -1 : 1;
  14972. duration = {
  14973. y: 0,
  14974. d: toInt(match[DATE]) * sign,
  14975. h: toInt(match[HOUR]) * sign,
  14976. m: toInt(match[MINUTE]) * sign,
  14977. s: toInt(match[SECOND]) * sign,
  14978. ms: toInt(match[MILLISECOND]) * sign
  14979. };
  14980. } else if (!!(match = isoDurationRegex.exec(input))) {
  14981. sign = (match[1] === "-") ? -1 : 1;
  14982. parseIso = function (inp) {
  14983. // We'd normally use ~~inp for this, but unfortunately it also
  14984. // converts floats to ints.
  14985. // inp may be undefined, so careful calling replace on it.
  14986. var res = inp && parseFloat(inp.replace(',', '.'));
  14987. // apply sign while we're at it
  14988. return (isNaN(res) ? 0 : res) * sign;
  14989. };
  14990. duration = {
  14991. y: parseIso(match[2]),
  14992. M: parseIso(match[3]),
  14993. d: parseIso(match[4]),
  14994. h: parseIso(match[5]),
  14995. m: parseIso(match[6]),
  14996. s: parseIso(match[7]),
  14997. w: parseIso(match[8])
  14998. };
  14999. }
  15000. ret = new Duration(duration);
  15001. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  15002. ret._lang = input._lang;
  15003. }
  15004. return ret;
  15005. };
  15006. // version number
  15007. moment.version = VERSION;
  15008. // default format
  15009. moment.defaultFormat = isoFormat;
  15010. // This function will be called whenever a moment is mutated.
  15011. // It is intended to keep the offset in sync with the timezone.
  15012. moment.updateOffset = function () {};
  15013. // This function will load languages and then set the global language. If
  15014. // no arguments are passed in, it will simply return the current global
  15015. // language key.
  15016. moment.lang = function (key, values) {
  15017. var r;
  15018. if (!key) {
  15019. return moment.fn._lang._abbr;
  15020. }
  15021. if (values) {
  15022. loadLang(normalizeLanguage(key), values);
  15023. } else if (values === null) {
  15024. unloadLang(key);
  15025. key = 'en';
  15026. } else if (!languages[key]) {
  15027. getLangDefinition(key);
  15028. }
  15029. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  15030. return r._abbr;
  15031. };
  15032. // returns language data
  15033. moment.langData = function (key) {
  15034. if (key && key._lang && key._lang._abbr) {
  15035. key = key._lang._abbr;
  15036. }
  15037. return getLangDefinition(key);
  15038. };
  15039. // compare moment object
  15040. moment.isMoment = function (obj) {
  15041. return obj instanceof Moment;
  15042. };
  15043. // for typechecking Duration objects
  15044. moment.isDuration = function (obj) {
  15045. return obj instanceof Duration;
  15046. };
  15047. for (i = lists.length - 1; i >= 0; --i) {
  15048. makeList(lists[i]);
  15049. }
  15050. moment.normalizeUnits = function (units) {
  15051. return normalizeUnits(units);
  15052. };
  15053. moment.invalid = function (flags) {
  15054. var m = moment.utc(NaN);
  15055. if (flags != null) {
  15056. extend(m._pf, flags);
  15057. }
  15058. else {
  15059. m._pf.userInvalidated = true;
  15060. }
  15061. return m;
  15062. };
  15063. moment.parseZone = function (input) {
  15064. return moment(input).parseZone();
  15065. };
  15066. /************************************
  15067. Moment Prototype
  15068. ************************************/
  15069. extend(moment.fn = Moment.prototype, {
  15070. clone : function () {
  15071. return moment(this);
  15072. },
  15073. valueOf : function () {
  15074. return +this._d + ((this._offset || 0) * 60000);
  15075. },
  15076. unix : function () {
  15077. return Math.floor(+this / 1000);
  15078. },
  15079. toString : function () {
  15080. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  15081. },
  15082. toDate : function () {
  15083. return this._offset ? new Date(+this) : this._d;
  15084. },
  15085. toISOString : function () {
  15086. var m = moment(this).utc();
  15087. if (0 < m.year() && m.year() <= 9999) {
  15088. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  15089. } else {
  15090. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  15091. }
  15092. },
  15093. toArray : function () {
  15094. var m = this;
  15095. return [
  15096. m.year(),
  15097. m.month(),
  15098. m.date(),
  15099. m.hours(),
  15100. m.minutes(),
  15101. m.seconds(),
  15102. m.milliseconds()
  15103. ];
  15104. },
  15105. isValid : function () {
  15106. return isValid(this);
  15107. },
  15108. isDSTShifted : function () {
  15109. if (this._a) {
  15110. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  15111. }
  15112. return false;
  15113. },
  15114. parsingFlags : function () {
  15115. return extend({}, this._pf);
  15116. },
  15117. invalidAt: function () {
  15118. return this._pf.overflow;
  15119. },
  15120. utc : function () {
  15121. return this.zone(0);
  15122. },
  15123. local : function () {
  15124. this.zone(0);
  15125. this._isUTC = false;
  15126. return this;
  15127. },
  15128. format : function (inputString) {
  15129. var output = formatMoment(this, inputString || moment.defaultFormat);
  15130. return this.lang().postformat(output);
  15131. },
  15132. add : function (input, val) {
  15133. var dur;
  15134. // switch args to support add('s', 1) and add(1, 's')
  15135. if (typeof input === 'string') {
  15136. dur = moment.duration(+val, input);
  15137. } else {
  15138. dur = moment.duration(input, val);
  15139. }
  15140. addOrSubtractDurationFromMoment(this, dur, 1);
  15141. return this;
  15142. },
  15143. subtract : function (input, val) {
  15144. var dur;
  15145. // switch args to support subtract('s', 1) and subtract(1, 's')
  15146. if (typeof input === 'string') {
  15147. dur = moment.duration(+val, input);
  15148. } else {
  15149. dur = moment.duration(input, val);
  15150. }
  15151. addOrSubtractDurationFromMoment(this, dur, -1);
  15152. return this;
  15153. },
  15154. diff : function (input, units, asFloat) {
  15155. var that = makeAs(input, this),
  15156. zoneDiff = (this.zone() - that.zone()) * 6e4,
  15157. diff, output;
  15158. units = normalizeUnits(units);
  15159. if (units === 'year' || units === 'month') {
  15160. // average number of days in the months in the given dates
  15161. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  15162. // difference in months
  15163. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  15164. // adjust by taking difference in days, average number of days
  15165. // and dst in the given months.
  15166. output += ((this - moment(this).startOf('month')) -
  15167. (that - moment(that).startOf('month'))) / diff;
  15168. // same as above but with zones, to negate all dst
  15169. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  15170. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  15171. if (units === 'year') {
  15172. output = output / 12;
  15173. }
  15174. } else {
  15175. diff = (this - that);
  15176. output = units === 'second' ? diff / 1e3 : // 1000
  15177. units === 'minute' ? diff / 6e4 : // 1000 * 60
  15178. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  15179. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  15180. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  15181. diff;
  15182. }
  15183. return asFloat ? output : absRound(output);
  15184. },
  15185. from : function (time, withoutSuffix) {
  15186. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  15187. },
  15188. fromNow : function (withoutSuffix) {
  15189. return this.from(moment(), withoutSuffix);
  15190. },
  15191. calendar : function () {
  15192. // We want to compare the start of today, vs this.
  15193. // Getting start-of-today depends on whether we're zone'd or not.
  15194. var sod = makeAs(moment(), this).startOf('day'),
  15195. diff = this.diff(sod, 'days', true),
  15196. format = diff < -6 ? 'sameElse' :
  15197. diff < -1 ? 'lastWeek' :
  15198. diff < 0 ? 'lastDay' :
  15199. diff < 1 ? 'sameDay' :
  15200. diff < 2 ? 'nextDay' :
  15201. diff < 7 ? 'nextWeek' : 'sameElse';
  15202. return this.format(this.lang().calendar(format, this));
  15203. },
  15204. isLeapYear : function () {
  15205. return isLeapYear(this.year());
  15206. },
  15207. isDST : function () {
  15208. return (this.zone() < this.clone().month(0).zone() ||
  15209. this.zone() < this.clone().month(5).zone());
  15210. },
  15211. day : function (input) {
  15212. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  15213. if (input != null) {
  15214. input = parseWeekday(input, this.lang());
  15215. return this.add({ d : input - day });
  15216. } else {
  15217. return day;
  15218. }
  15219. },
  15220. month : function (input) {
  15221. var utc = this._isUTC ? 'UTC' : '',
  15222. dayOfMonth;
  15223. if (input != null) {
  15224. if (typeof input === 'string') {
  15225. input = this.lang().monthsParse(input);
  15226. if (typeof input !== 'number') {
  15227. return this;
  15228. }
  15229. }
  15230. dayOfMonth = this.date();
  15231. this.date(1);
  15232. this._d['set' + utc + 'Month'](input);
  15233. this.date(Math.min(dayOfMonth, this.daysInMonth()));
  15234. moment.updateOffset(this);
  15235. return this;
  15236. } else {
  15237. return this._d['get' + utc + 'Month']();
  15238. }
  15239. },
  15240. startOf: function (units) {
  15241. units = normalizeUnits(units);
  15242. // the following switch intentionally omits break keywords
  15243. // to utilize falling through the cases.
  15244. switch (units) {
  15245. case 'year':
  15246. this.month(0);
  15247. /* falls through */
  15248. case 'month':
  15249. this.date(1);
  15250. /* falls through */
  15251. case 'week':
  15252. case 'isoWeek':
  15253. case 'day':
  15254. this.hours(0);
  15255. /* falls through */
  15256. case 'hour':
  15257. this.minutes(0);
  15258. /* falls through */
  15259. case 'minute':
  15260. this.seconds(0);
  15261. /* falls through */
  15262. case 'second':
  15263. this.milliseconds(0);
  15264. /* falls through */
  15265. }
  15266. // weeks are a special case
  15267. if (units === 'week') {
  15268. this.weekday(0);
  15269. } else if (units === 'isoWeek') {
  15270. this.isoWeekday(1);
  15271. }
  15272. return this;
  15273. },
  15274. endOf: function (units) {
  15275. units = normalizeUnits(units);
  15276. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  15277. },
  15278. isAfter: function (input, units) {
  15279. units = typeof units !== 'undefined' ? units : 'millisecond';
  15280. return +this.clone().startOf(units) > +moment(input).startOf(units);
  15281. },
  15282. isBefore: function (input, units) {
  15283. units = typeof units !== 'undefined' ? units : 'millisecond';
  15284. return +this.clone().startOf(units) < +moment(input).startOf(units);
  15285. },
  15286. isSame: function (input, units) {
  15287. units = units || 'ms';
  15288. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  15289. },
  15290. min: function (other) {
  15291. other = moment.apply(null, arguments);
  15292. return other < this ? this : other;
  15293. },
  15294. max: function (other) {
  15295. other = moment.apply(null, arguments);
  15296. return other > this ? this : other;
  15297. },
  15298. zone : function (input) {
  15299. var offset = this._offset || 0;
  15300. if (input != null) {
  15301. if (typeof input === "string") {
  15302. input = timezoneMinutesFromString(input);
  15303. }
  15304. if (Math.abs(input) < 16) {
  15305. input = input * 60;
  15306. }
  15307. this._offset = input;
  15308. this._isUTC = true;
  15309. if (offset !== input) {
  15310. addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
  15311. }
  15312. } else {
  15313. return this._isUTC ? offset : this._d.getTimezoneOffset();
  15314. }
  15315. return this;
  15316. },
  15317. zoneAbbr : function () {
  15318. return this._isUTC ? "UTC" : "";
  15319. },
  15320. zoneName : function () {
  15321. return this._isUTC ? "Coordinated Universal Time" : "";
  15322. },
  15323. parseZone : function () {
  15324. if (this._tzm) {
  15325. this.zone(this._tzm);
  15326. } else if (typeof this._i === 'string') {
  15327. this.zone(this._i);
  15328. }
  15329. return this;
  15330. },
  15331. hasAlignedHourOffset : function (input) {
  15332. if (!input) {
  15333. input = 0;
  15334. }
  15335. else {
  15336. input = moment(input).zone();
  15337. }
  15338. return (this.zone() - input) % 60 === 0;
  15339. },
  15340. daysInMonth : function () {
  15341. return daysInMonth(this.year(), this.month());
  15342. },
  15343. dayOfYear : function (input) {
  15344. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  15345. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  15346. },
  15347. quarter : function () {
  15348. return Math.ceil((this.month() + 1.0) / 3.0);
  15349. },
  15350. weekYear : function (input) {
  15351. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  15352. return input == null ? year : this.add("y", (input - year));
  15353. },
  15354. isoWeekYear : function (input) {
  15355. var year = weekOfYear(this, 1, 4).year;
  15356. return input == null ? year : this.add("y", (input - year));
  15357. },
  15358. week : function (input) {
  15359. var week = this.lang().week(this);
  15360. return input == null ? week : this.add("d", (input - week) * 7);
  15361. },
  15362. isoWeek : function (input) {
  15363. var week = weekOfYear(this, 1, 4).week;
  15364. return input == null ? week : this.add("d", (input - week) * 7);
  15365. },
  15366. weekday : function (input) {
  15367. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  15368. return input == null ? weekday : this.add("d", input - weekday);
  15369. },
  15370. isoWeekday : function (input) {
  15371. // behaves the same as moment#day except
  15372. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  15373. // as a setter, sunday should belong to the previous week.
  15374. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  15375. },
  15376. get : function (units) {
  15377. units = normalizeUnits(units);
  15378. return this[units]();
  15379. },
  15380. set : function (units, value) {
  15381. units = normalizeUnits(units);
  15382. if (typeof this[units] === 'function') {
  15383. this[units](value);
  15384. }
  15385. return this;
  15386. },
  15387. // If passed a language key, it will set the language for this
  15388. // instance. Otherwise, it will return the language configuration
  15389. // variables for this instance.
  15390. lang : function (key) {
  15391. if (key === undefined) {
  15392. return this._lang;
  15393. } else {
  15394. this._lang = getLangDefinition(key);
  15395. return this;
  15396. }
  15397. }
  15398. });
  15399. // helper for adding shortcuts
  15400. function makeGetterAndSetter(name, key) {
  15401. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  15402. var utc = this._isUTC ? 'UTC' : '';
  15403. if (input != null) {
  15404. this._d['set' + utc + key](input);
  15405. moment.updateOffset(this);
  15406. return this;
  15407. } else {
  15408. return this._d['get' + utc + key]();
  15409. }
  15410. };
  15411. }
  15412. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  15413. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  15414. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  15415. }
  15416. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  15417. makeGetterAndSetter('year', 'FullYear');
  15418. // add plural methods
  15419. moment.fn.days = moment.fn.day;
  15420. moment.fn.months = moment.fn.month;
  15421. moment.fn.weeks = moment.fn.week;
  15422. moment.fn.isoWeeks = moment.fn.isoWeek;
  15423. // add aliased format methods
  15424. moment.fn.toJSON = moment.fn.toISOString;
  15425. /************************************
  15426. Duration Prototype
  15427. ************************************/
  15428. extend(moment.duration.fn = Duration.prototype, {
  15429. _bubble : function () {
  15430. var milliseconds = this._milliseconds,
  15431. days = this._days,
  15432. months = this._months,
  15433. data = this._data,
  15434. seconds, minutes, hours, years;
  15435. // The following code bubbles up values, see the tests for
  15436. // examples of what that means.
  15437. data.milliseconds = milliseconds % 1000;
  15438. seconds = absRound(milliseconds / 1000);
  15439. data.seconds = seconds % 60;
  15440. minutes = absRound(seconds / 60);
  15441. data.minutes = minutes % 60;
  15442. hours = absRound(minutes / 60);
  15443. data.hours = hours % 24;
  15444. days += absRound(hours / 24);
  15445. data.days = days % 30;
  15446. months += absRound(days / 30);
  15447. data.months = months % 12;
  15448. years = absRound(months / 12);
  15449. data.years = years;
  15450. },
  15451. weeks : function () {
  15452. return absRound(this.days() / 7);
  15453. },
  15454. valueOf : function () {
  15455. return this._milliseconds +
  15456. this._days * 864e5 +
  15457. (this._months % 12) * 2592e6 +
  15458. toInt(this._months / 12) * 31536e6;
  15459. },
  15460. humanize : function (withSuffix) {
  15461. var difference = +this,
  15462. output = relativeTime(difference, !withSuffix, this.lang());
  15463. if (withSuffix) {
  15464. output = this.lang().pastFuture(difference, output);
  15465. }
  15466. return this.lang().postformat(output);
  15467. },
  15468. add : function (input, val) {
  15469. // supports only 2.0-style add(1, 's') or add(moment)
  15470. var dur = moment.duration(input, val);
  15471. this._milliseconds += dur._milliseconds;
  15472. this._days += dur._days;
  15473. this._months += dur._months;
  15474. this._bubble();
  15475. return this;
  15476. },
  15477. subtract : function (input, val) {
  15478. var dur = moment.duration(input, val);
  15479. this._milliseconds -= dur._milliseconds;
  15480. this._days -= dur._days;
  15481. this._months -= dur._months;
  15482. this._bubble();
  15483. return this;
  15484. },
  15485. get : function (units) {
  15486. units = normalizeUnits(units);
  15487. return this[units.toLowerCase() + 's']();
  15488. },
  15489. as : function (units) {
  15490. units = normalizeUnits(units);
  15491. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  15492. },
  15493. lang : moment.fn.lang,
  15494. toIsoString : function () {
  15495. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  15496. var years = Math.abs(this.years()),
  15497. months = Math.abs(this.months()),
  15498. days = Math.abs(this.days()),
  15499. hours = Math.abs(this.hours()),
  15500. minutes = Math.abs(this.minutes()),
  15501. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  15502. if (!this.asSeconds()) {
  15503. // this is the same as C#'s (Noda) and python (isodate)...
  15504. // but not other JS (goog.date)
  15505. return 'P0D';
  15506. }
  15507. return (this.asSeconds() < 0 ? '-' : '') +
  15508. 'P' +
  15509. (years ? years + 'Y' : '') +
  15510. (months ? months + 'M' : '') +
  15511. (days ? days + 'D' : '') +
  15512. ((hours || minutes || seconds) ? 'T' : '') +
  15513. (hours ? hours + 'H' : '') +
  15514. (minutes ? minutes + 'M' : '') +
  15515. (seconds ? seconds + 'S' : '');
  15516. }
  15517. });
  15518. function makeDurationGetter(name) {
  15519. moment.duration.fn[name] = function () {
  15520. return this._data[name];
  15521. };
  15522. }
  15523. function makeDurationAsGetter(name, factor) {
  15524. moment.duration.fn['as' + name] = function () {
  15525. return +this / factor;
  15526. };
  15527. }
  15528. for (i in unitMillisecondFactors) {
  15529. if (unitMillisecondFactors.hasOwnProperty(i)) {
  15530. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  15531. makeDurationGetter(i.toLowerCase());
  15532. }
  15533. }
  15534. makeDurationAsGetter('Weeks', 6048e5);
  15535. moment.duration.fn.asMonths = function () {
  15536. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  15537. };
  15538. /************************************
  15539. Default Lang
  15540. ************************************/
  15541. // Set default language, other languages will inherit from English.
  15542. moment.lang('en', {
  15543. ordinal : function (number) {
  15544. var b = number % 10,
  15545. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  15546. (b === 1) ? 'st' :
  15547. (b === 2) ? 'nd' :
  15548. (b === 3) ? 'rd' : 'th';
  15549. return number + output;
  15550. }
  15551. });
  15552. /* EMBED_LANGUAGES */
  15553. /************************************
  15554. Exposing Moment
  15555. ************************************/
  15556. function makeGlobal(deprecate) {
  15557. var warned = false, local_moment = moment;
  15558. /*global ender:false */
  15559. if (typeof ender !== 'undefined') {
  15560. return;
  15561. }
  15562. // here, `this` means `window` in the browser, or `global` on the server
  15563. // add `moment` as a global object via a string identifier,
  15564. // for Closure Compiler "advanced" mode
  15565. if (deprecate) {
  15566. global.moment = function () {
  15567. if (!warned && console && console.warn) {
  15568. warned = true;
  15569. console.warn(
  15570. "Accessing Moment through the global scope is " +
  15571. "deprecated, and will be removed in an upcoming " +
  15572. "release.");
  15573. }
  15574. return local_moment.apply(null, arguments);
  15575. };
  15576. extend(global.moment, local_moment);
  15577. } else {
  15578. global['moment'] = moment;
  15579. }
  15580. }
  15581. // CommonJS module is defined
  15582. if (hasModule) {
  15583. module.exports = moment;
  15584. makeGlobal(true);
  15585. } else if (typeof define === "function" && define.amd) {
  15586. define("moment", function (require, exports, module) {
  15587. if (module.config && module.config() && module.config().noGlobal !== true) {
  15588. // If user provided noGlobal, he is aware of global
  15589. makeGlobal(module.config().noGlobal === undefined);
  15590. }
  15591. return moment;
  15592. });
  15593. } else {
  15594. makeGlobal();
  15595. }
  15596. }).call(this);
  15597. },{}],4:[function(require,module,exports){
  15598. /**
  15599. * Copyright 2012 Craig Campbell
  15600. *
  15601. * Licensed under the Apache License, Version 2.0 (the "License");
  15602. * you may not use this file except in compliance with the License.
  15603. * You may obtain a copy of the License at
  15604. *
  15605. * http://www.apache.org/licenses/LICENSE-2.0
  15606. *
  15607. * Unless required by applicable law or agreed to in writing, software
  15608. * distributed under the License is distributed on an "AS IS" BASIS,
  15609. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15610. * See the License for the specific language governing permissions and
  15611. * limitations under the License.
  15612. *
  15613. * Mousetrap is a simple keyboard shortcut library for Javascript with
  15614. * no external dependencies
  15615. *
  15616. * @version 1.1.2
  15617. * @url craig.is/killing/mice
  15618. */
  15619. /**
  15620. * mapping of special keycodes to their corresponding keys
  15621. *
  15622. * everything in this dictionary cannot use keypress events
  15623. * so it has to be here to map to the correct keycodes for
  15624. * keyup/keydown events
  15625. *
  15626. * @type {Object}
  15627. */
  15628. var _MAP = {
  15629. 8: 'backspace',
  15630. 9: 'tab',
  15631. 13: 'enter',
  15632. 16: 'shift',
  15633. 17: 'ctrl',
  15634. 18: 'alt',
  15635. 20: 'capslock',
  15636. 27: 'esc',
  15637. 32: 'space',
  15638. 33: 'pageup',
  15639. 34: 'pagedown',
  15640. 35: 'end',
  15641. 36: 'home',
  15642. 37: 'left',
  15643. 38: 'up',
  15644. 39: 'right',
  15645. 40: 'down',
  15646. 45: 'ins',
  15647. 46: 'del',
  15648. 91: 'meta',
  15649. 93: 'meta',
  15650. 224: 'meta'
  15651. },
  15652. /**
  15653. * mapping for special characters so they can support
  15654. *
  15655. * this dictionary is only used incase you want to bind a
  15656. * keyup or keydown event to one of these keys
  15657. *
  15658. * @type {Object}
  15659. */
  15660. _KEYCODE_MAP = {
  15661. 106: '*',
  15662. 107: '+',
  15663. 109: '-',
  15664. 110: '.',
  15665. 111 : '/',
  15666. 186: ';',
  15667. 187: '=',
  15668. 188: ',',
  15669. 189: '-',
  15670. 190: '.',
  15671. 191: '/',
  15672. 192: '`',
  15673. 219: '[',
  15674. 220: '\\',
  15675. 221: ']',
  15676. 222: '\''
  15677. },
  15678. /**
  15679. * this is a mapping of keys that require shift on a US keypad
  15680. * back to the non shift equivelents
  15681. *
  15682. * this is so you can use keyup events with these keys
  15683. *
  15684. * note that this will only work reliably on US keyboards
  15685. *
  15686. * @type {Object}
  15687. */
  15688. _SHIFT_MAP = {
  15689. '~': '`',
  15690. '!': '1',
  15691. '@': '2',
  15692. '#': '3',
  15693. '$': '4',
  15694. '%': '5',
  15695. '^': '6',
  15696. '&': '7',
  15697. '*': '8',
  15698. '(': '9',
  15699. ')': '0',
  15700. '_': '-',
  15701. '+': '=',
  15702. ':': ';',
  15703. '\"': '\'',
  15704. '<': ',',
  15705. '>': '.',
  15706. '?': '/',
  15707. '|': '\\'
  15708. },
  15709. /**
  15710. * this is a list of special strings you can use to map
  15711. * to modifier keys when you specify your keyboard shortcuts
  15712. *
  15713. * @type {Object}
  15714. */
  15715. _SPECIAL_ALIASES = {
  15716. 'option': 'alt',
  15717. 'command': 'meta',
  15718. 'return': 'enter',
  15719. 'escape': 'esc'
  15720. },
  15721. /**
  15722. * variable to store the flipped version of _MAP from above
  15723. * needed to check if we should use keypress or not when no action
  15724. * is specified
  15725. *
  15726. * @type {Object|undefined}
  15727. */
  15728. _REVERSE_MAP,
  15729. /**
  15730. * a list of all the callbacks setup via Mousetrap.bind()
  15731. *
  15732. * @type {Object}
  15733. */
  15734. _callbacks = {},
  15735. /**
  15736. * direct map of string combinations to callbacks used for trigger()
  15737. *
  15738. * @type {Object}
  15739. */
  15740. _direct_map = {},
  15741. /**
  15742. * keeps track of what level each sequence is at since multiple
  15743. * sequences can start out with the same sequence
  15744. *
  15745. * @type {Object}
  15746. */
  15747. _sequence_levels = {},
  15748. /**
  15749. * variable to store the setTimeout call
  15750. *
  15751. * @type {null|number}
  15752. */
  15753. _reset_timer,
  15754. /**
  15755. * temporary state where we will ignore the next keyup
  15756. *
  15757. * @type {boolean|string}
  15758. */
  15759. _ignore_next_keyup = false,
  15760. /**
  15761. * are we currently inside of a sequence?
  15762. * type of action ("keyup" or "keydown" or "keypress") or false
  15763. *
  15764. * @type {boolean|string}
  15765. */
  15766. _inside_sequence = false;
  15767. /**
  15768. * loop through the f keys, f1 to f19 and add them to the map
  15769. * programatically
  15770. */
  15771. for (var i = 1; i < 20; ++i) {
  15772. _MAP[111 + i] = 'f' + i;
  15773. }
  15774. /**
  15775. * loop through to map numbers on the numeric keypad
  15776. */
  15777. for (i = 0; i <= 9; ++i) {
  15778. _MAP[i + 96] = i;
  15779. }
  15780. /**
  15781. * cross browser add event method
  15782. *
  15783. * @param {Element|HTMLDocument} object
  15784. * @param {string} type
  15785. * @param {Function} callback
  15786. * @returns void
  15787. */
  15788. function _addEvent(object, type, callback) {
  15789. if (object.addEventListener) {
  15790. return object.addEventListener(type, callback, false);
  15791. }
  15792. object.attachEvent('on' + type, callback);
  15793. }
  15794. /**
  15795. * takes the event and returns the key character
  15796. *
  15797. * @param {Event} e
  15798. * @return {string}
  15799. */
  15800. function _characterFromEvent(e) {
  15801. // for keypress events we should return the character as is
  15802. if (e.type == 'keypress') {
  15803. return String.fromCharCode(e.which);
  15804. }
  15805. // for non keypress events the special maps are needed
  15806. if (_MAP[e.which]) {
  15807. return _MAP[e.which];
  15808. }
  15809. if (_KEYCODE_MAP[e.which]) {
  15810. return _KEYCODE_MAP[e.which];
  15811. }
  15812. // if it is not in the special map
  15813. return String.fromCharCode(e.which).toLowerCase();
  15814. }
  15815. /**
  15816. * should we stop this event before firing off callbacks
  15817. *
  15818. * @param {Event} e
  15819. * @return {boolean}
  15820. */
  15821. function _stop(e) {
  15822. var element = e.target || e.srcElement,
  15823. tag_name = element.tagName;
  15824. // if the element has the class "mousetrap" then no need to stop
  15825. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  15826. return false;
  15827. }
  15828. // stop for input, select, and textarea
  15829. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  15830. }
  15831. /**
  15832. * checks if two arrays are equal
  15833. *
  15834. * @param {Array} modifiers1
  15835. * @param {Array} modifiers2
  15836. * @returns {boolean}
  15837. */
  15838. function _modifiersMatch(modifiers1, modifiers2) {
  15839. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  15840. }
  15841. /**
  15842. * resets all sequence counters except for the ones passed in
  15843. *
  15844. * @param {Object} do_not_reset
  15845. * @returns void
  15846. */
  15847. function _resetSequences(do_not_reset) {
  15848. do_not_reset = do_not_reset || {};
  15849. var active_sequences = false,
  15850. key;
  15851. for (key in _sequence_levels) {
  15852. if (do_not_reset[key]) {
  15853. active_sequences = true;
  15854. continue;
  15855. }
  15856. _sequence_levels[key] = 0;
  15857. }
  15858. if (!active_sequences) {
  15859. _inside_sequence = false;
  15860. }
  15861. }
  15862. /**
  15863. * finds all callbacks that match based on the keycode, modifiers,
  15864. * and action
  15865. *
  15866. * @param {string} character
  15867. * @param {Array} modifiers
  15868. * @param {string} action
  15869. * @param {boolean=} remove - should we remove any matches
  15870. * @param {string=} combination
  15871. * @returns {Array}
  15872. */
  15873. function _getMatches(character, modifiers, action, remove, combination) {
  15874. var i,
  15875. callback,
  15876. matches = [];
  15877. // if there are no events related to this keycode
  15878. if (!_callbacks[character]) {
  15879. return [];
  15880. }
  15881. // if a modifier key is coming up on its own we should allow it
  15882. if (action == 'keyup' && _isModifier(character)) {
  15883. modifiers = [character];
  15884. }
  15885. // loop through all callbacks for the key that was pressed
  15886. // and see if any of them match
  15887. for (i = 0; i < _callbacks[character].length; ++i) {
  15888. callback = _callbacks[character][i];
  15889. // if this is a sequence but it is not at the right level
  15890. // then move onto the next match
  15891. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  15892. continue;
  15893. }
  15894. // if the action we are looking for doesn't match the action we got
  15895. // then we should keep going
  15896. if (action != callback.action) {
  15897. continue;
  15898. }
  15899. // if this is a keypress event that means that we need to only
  15900. // look at the character, otherwise check the modifiers as
  15901. // well
  15902. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  15903. // remove is used so if you change your mind and call bind a
  15904. // second time with a new function the first one is overwritten
  15905. if (remove && callback.combo == combination) {
  15906. _callbacks[character].splice(i, 1);
  15907. }
  15908. matches.push(callback);
  15909. }
  15910. }
  15911. return matches;
  15912. }
  15913. /**
  15914. * takes a key event and figures out what the modifiers are
  15915. *
  15916. * @param {Event} e
  15917. * @returns {Array}
  15918. */
  15919. function _eventModifiers(e) {
  15920. var modifiers = [];
  15921. if (e.shiftKey) {
  15922. modifiers.push('shift');
  15923. }
  15924. if (e.altKey) {
  15925. modifiers.push('alt');
  15926. }
  15927. if (e.ctrlKey) {
  15928. modifiers.push('ctrl');
  15929. }
  15930. if (e.metaKey) {
  15931. modifiers.push('meta');
  15932. }
  15933. return modifiers;
  15934. }
  15935. /**
  15936. * actually calls the callback function
  15937. *
  15938. * if your callback function returns false this will use the jquery
  15939. * convention - prevent default and stop propogation on the event
  15940. *
  15941. * @param {Function} callback
  15942. * @param {Event} e
  15943. * @returns void
  15944. */
  15945. function _fireCallback(callback, e) {
  15946. if (callback(e) === false) {
  15947. if (e.preventDefault) {
  15948. e.preventDefault();
  15949. }
  15950. if (e.stopPropagation) {
  15951. e.stopPropagation();
  15952. }
  15953. e.returnValue = false;
  15954. e.cancelBubble = true;
  15955. }
  15956. }
  15957. /**
  15958. * handles a character key event
  15959. *
  15960. * @param {string} character
  15961. * @param {Event} e
  15962. * @returns void
  15963. */
  15964. function _handleCharacter(character, e) {
  15965. // if this event should not happen stop here
  15966. if (_stop(e)) {
  15967. return;
  15968. }
  15969. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  15970. i,
  15971. do_not_reset = {},
  15972. processed_sequence_callback = false;
  15973. // loop through matching callbacks for this key event
  15974. for (i = 0; i < callbacks.length; ++i) {
  15975. // fire for all sequence callbacks
  15976. // this is because if for example you have multiple sequences
  15977. // bound such as "g i" and "g t" they both need to fire the
  15978. // callback for matching g cause otherwise you can only ever
  15979. // match the first one
  15980. if (callbacks[i].seq) {
  15981. processed_sequence_callback = true;
  15982. // keep a list of which sequences were matches for later
  15983. do_not_reset[callbacks[i].seq] = 1;
  15984. _fireCallback(callbacks[i].callback, e);
  15985. continue;
  15986. }
  15987. // if there were no sequence matches but we are still here
  15988. // that means this is a regular match so we should fire that
  15989. if (!processed_sequence_callback && !_inside_sequence) {
  15990. _fireCallback(callbacks[i].callback, e);
  15991. }
  15992. }
  15993. // if you are inside of a sequence and the key you are pressing
  15994. // is not a modifier key then we should reset all sequences
  15995. // that were not matched by this key event
  15996. if (e.type == _inside_sequence && !_isModifier(character)) {
  15997. _resetSequences(do_not_reset);
  15998. }
  15999. }
  16000. /**
  16001. * handles a keydown event
  16002. *
  16003. * @param {Event} e
  16004. * @returns void
  16005. */
  16006. function _handleKey(e) {
  16007. // normalize e.which for key events
  16008. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  16009. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  16010. var character = _characterFromEvent(e);
  16011. // no character found then stop
  16012. if (!character) {
  16013. return;
  16014. }
  16015. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  16016. _ignore_next_keyup = false;
  16017. return;
  16018. }
  16019. _handleCharacter(character, e);
  16020. }
  16021. /**
  16022. * determines if the keycode specified is a modifier key or not
  16023. *
  16024. * @param {string} key
  16025. * @returns {boolean}
  16026. */
  16027. function _isModifier(key) {
  16028. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  16029. }
  16030. /**
  16031. * called to set a 1 second timeout on the specified sequence
  16032. *
  16033. * this is so after each key press in the sequence you have 1 second
  16034. * to press the next key before you have to start over
  16035. *
  16036. * @returns void
  16037. */
  16038. function _resetSequenceTimer() {
  16039. clearTimeout(_reset_timer);
  16040. _reset_timer = setTimeout(_resetSequences, 1000);
  16041. }
  16042. /**
  16043. * reverses the map lookup so that we can look for specific keys
  16044. * to see what can and can't use keypress
  16045. *
  16046. * @return {Object}
  16047. */
  16048. function _getReverseMap() {
  16049. if (!_REVERSE_MAP) {
  16050. _REVERSE_MAP = {};
  16051. for (var key in _MAP) {
  16052. // pull out the numeric keypad from here cause keypress should
  16053. // be able to detect the keys from the character
  16054. if (key > 95 && key < 112) {
  16055. continue;
  16056. }
  16057. if (_MAP.hasOwnProperty(key)) {
  16058. _REVERSE_MAP[_MAP[key]] = key;
  16059. }
  16060. }
  16061. }
  16062. return _REVERSE_MAP;
  16063. }
  16064. /**
  16065. * picks the best action based on the key combination
  16066. *
  16067. * @param {string} key - character for key
  16068. * @param {Array} modifiers
  16069. * @param {string=} action passed in
  16070. */
  16071. function _pickBestAction(key, modifiers, action) {
  16072. // if no action was picked in we should try to pick the one
  16073. // that we think would work best for this key
  16074. if (!action) {
  16075. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  16076. }
  16077. // modifier keys don't work as expected with keypress,
  16078. // switch to keydown
  16079. if (action == 'keypress' && modifiers.length) {
  16080. action = 'keydown';
  16081. }
  16082. return action;
  16083. }
  16084. /**
  16085. * binds a key sequence to an event
  16086. *
  16087. * @param {string} combo - combo specified in bind call
  16088. * @param {Array} keys
  16089. * @param {Function} callback
  16090. * @param {string=} action
  16091. * @returns void
  16092. */
  16093. function _bindSequence(combo, keys, callback, action) {
  16094. // start off by adding a sequence level record for this combination
  16095. // and setting the level to 0
  16096. _sequence_levels[combo] = 0;
  16097. // if there is no action pick the best one for the first key
  16098. // in the sequence
  16099. if (!action) {
  16100. action = _pickBestAction(keys[0], []);
  16101. }
  16102. /**
  16103. * callback to increase the sequence level for this sequence and reset
  16104. * all other sequences that were active
  16105. *
  16106. * @param {Event} e
  16107. * @returns void
  16108. */
  16109. var _increaseSequence = function(e) {
  16110. _inside_sequence = action;
  16111. ++_sequence_levels[combo];
  16112. _resetSequenceTimer();
  16113. },
  16114. /**
  16115. * wraps the specified callback inside of another function in order
  16116. * to reset all sequence counters as soon as this sequence is done
  16117. *
  16118. * @param {Event} e
  16119. * @returns void
  16120. */
  16121. _callbackAndReset = function(e) {
  16122. _fireCallback(callback, e);
  16123. // we should ignore the next key up if the action is key down
  16124. // or keypress. this is so if you finish a sequence and
  16125. // release the key the final key will not trigger a keyup
  16126. if (action !== 'keyup') {
  16127. _ignore_next_keyup = _characterFromEvent(e);
  16128. }
  16129. // weird race condition if a sequence ends with the key
  16130. // another sequence begins with
  16131. setTimeout(_resetSequences, 10);
  16132. },
  16133. i;
  16134. // loop through keys one at a time and bind the appropriate callback
  16135. // function. for any key leading up to the final one it should
  16136. // increase the sequence. after the final, it should reset all sequences
  16137. for (i = 0; i < keys.length; ++i) {
  16138. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  16139. }
  16140. }
  16141. /**
  16142. * binds a single keyboard combination
  16143. *
  16144. * @param {string} combination
  16145. * @param {Function} callback
  16146. * @param {string=} action
  16147. * @param {string=} sequence_name - name of sequence if part of sequence
  16148. * @param {number=} level - what part of the sequence the command is
  16149. * @returns void
  16150. */
  16151. function _bindSingle(combination, callback, action, sequence_name, level) {
  16152. // make sure multiple spaces in a row become a single space
  16153. combination = combination.replace(/\s+/g, ' ');
  16154. var sequence = combination.split(' '),
  16155. i,
  16156. key,
  16157. keys,
  16158. modifiers = [];
  16159. // if this pattern is a sequence of keys then run through this method
  16160. // to reprocess each pattern one key at a time
  16161. if (sequence.length > 1) {
  16162. return _bindSequence(combination, sequence, callback, action);
  16163. }
  16164. // take the keys from this pattern and figure out what the actual
  16165. // pattern is all about
  16166. keys = combination === '+' ? ['+'] : combination.split('+');
  16167. for (i = 0; i < keys.length; ++i) {
  16168. key = keys[i];
  16169. // normalize key names
  16170. if (_SPECIAL_ALIASES[key]) {
  16171. key = _SPECIAL_ALIASES[key];
  16172. }
  16173. // if this is not a keypress event then we should
  16174. // be smart about using shift keys
  16175. // this will only work for US keyboards however
  16176. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  16177. key = _SHIFT_MAP[key];
  16178. modifiers.push('shift');
  16179. }
  16180. // if this key is a modifier then add it to the list of modifiers
  16181. if (_isModifier(key)) {
  16182. modifiers.push(key);
  16183. }
  16184. }
  16185. // depending on what the key combination is
  16186. // we will try to pick the best event for it
  16187. action = _pickBestAction(key, modifiers, action);
  16188. // make sure to initialize array if this is the first time
  16189. // a callback is added for this key
  16190. if (!_callbacks[key]) {
  16191. _callbacks[key] = [];
  16192. }
  16193. // remove an existing match if there is one
  16194. _getMatches(key, modifiers, action, !sequence_name, combination);
  16195. // add this call back to the array
  16196. // if it is a sequence put it at the beginning
  16197. // if not put it at the end
  16198. //
  16199. // this is important because the way these are processed expects
  16200. // the sequence ones to come first
  16201. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  16202. callback: callback,
  16203. modifiers: modifiers,
  16204. action: action,
  16205. seq: sequence_name,
  16206. level: level,
  16207. combo: combination
  16208. });
  16209. }
  16210. /**
  16211. * binds multiple combinations to the same callback
  16212. *
  16213. * @param {Array} combinations
  16214. * @param {Function} callback
  16215. * @param {string|undefined} action
  16216. * @returns void
  16217. */
  16218. function _bindMultiple(combinations, callback, action) {
  16219. for (var i = 0; i < combinations.length; ++i) {
  16220. _bindSingle(combinations[i], callback, action);
  16221. }
  16222. }
  16223. // start!
  16224. _addEvent(document, 'keypress', _handleKey);
  16225. _addEvent(document, 'keydown', _handleKey);
  16226. _addEvent(document, 'keyup', _handleKey);
  16227. var mousetrap = {
  16228. /**
  16229. * binds an event to mousetrap
  16230. *
  16231. * can be a single key, a combination of keys separated with +,
  16232. * a comma separated list of keys, an array of keys, or
  16233. * a sequence of keys separated by spaces
  16234. *
  16235. * be sure to list the modifier keys first to make sure that the
  16236. * correct key ends up getting bound (the last key in the pattern)
  16237. *
  16238. * @param {string|Array} keys
  16239. * @param {Function} callback
  16240. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  16241. * @returns void
  16242. */
  16243. bind: function(keys, callback, action) {
  16244. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  16245. _direct_map[keys + ':' + action] = callback;
  16246. return this;
  16247. },
  16248. /**
  16249. * unbinds an event to mousetrap
  16250. *
  16251. * the unbinding sets the callback function of the specified key combo
  16252. * to an empty function and deletes the corresponding key in the
  16253. * _direct_map dict.
  16254. *
  16255. * the keycombo+action has to be exactly the same as
  16256. * it was defined in the bind method
  16257. *
  16258. * TODO: actually remove this from the _callbacks dictionary instead
  16259. * of binding an empty function
  16260. *
  16261. * @param {string|Array} keys
  16262. * @param {string} action
  16263. * @returns void
  16264. */
  16265. unbind: function(keys, action) {
  16266. if (_direct_map[keys + ':' + action]) {
  16267. delete _direct_map[keys + ':' + action];
  16268. this.bind(keys, function() {}, action);
  16269. }
  16270. return this;
  16271. },
  16272. /**
  16273. * triggers an event that has already been bound
  16274. *
  16275. * @param {string} keys
  16276. * @param {string=} action
  16277. * @returns void
  16278. */
  16279. trigger: function(keys, action) {
  16280. _direct_map[keys + ':' + action]();
  16281. return this;
  16282. },
  16283. /**
  16284. * resets the library back to its initial state. this is useful
  16285. * if you want to clear out the current keyboard shortcuts and bind
  16286. * new ones - for example if you switch to another page
  16287. *
  16288. * @returns void
  16289. */
  16290. reset: function() {
  16291. _callbacks = {};
  16292. _direct_map = {};
  16293. return this;
  16294. }
  16295. };
  16296. module.exports = mousetrap;
  16297. },{}]},{},[1])
  16298. (1)
  16299. });