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.

17863 lines
505 KiB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. * @version @@version
  8. * @date @@date
  9. *
  10. * @license
  11. * Copyright (C) 2011-2013 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. /*! Hammer.JS - v1.0.5 - 2013-04-07
  27. * http://eightmedia.github.com/hammer.js
  28. *
  29. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  30. * Licensed under the MIT license */
  31. (function(window, undefined) {
  32. 'use strict';
  33. /**
  34. * Hammer
  35. * use this to create instances
  36. * @param {HTMLElement} element
  37. * @param {Object} options
  38. * @returns {Hammer.Instance}
  39. * @constructor
  40. */
  41. var Hammer = function(element, options) {
  42. return new Hammer.Instance(element, options || {});
  43. };
  44. // default settings
  45. Hammer.defaults = {
  46. // add styles and attributes to the element to prevent the browser from doing
  47. // its native behavior. this doesnt prevent the scrolling, but cancels
  48. // the contextmenu, tap highlighting etc
  49. // set to false to disable this
  50. stop_browser_behavior: {
  51. // this also triggers onselectstart=false for IE
  52. userSelect: 'none',
  53. // this makes the element blocking in IE10 >, you could experiment with the value
  54. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  55. touchAction: 'none',
  56. touchCallout: 'none',
  57. contentZooming: 'none',
  58. userDrag: 'none',
  59. tapHighlightColor: 'rgba(0,0,0,0)'
  60. }
  61. // more settings are defined per gesture at gestures.js
  62. };
  63. // detect touchevents
  64. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  65. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  66. // dont use mouseevents on mobile devices
  67. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  68. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  69. // eventtypes per touchevent (start, move, end)
  70. // are filled by Hammer.event.determineEventTypes on setup
  71. Hammer.EVENT_TYPES = {};
  72. // direction defines
  73. Hammer.DIRECTION_DOWN = 'down';
  74. Hammer.DIRECTION_LEFT = 'left';
  75. Hammer.DIRECTION_UP = 'up';
  76. Hammer.DIRECTION_RIGHT = 'right';
  77. // pointer type
  78. Hammer.POINTER_MOUSE = 'mouse';
  79. Hammer.POINTER_TOUCH = 'touch';
  80. Hammer.POINTER_PEN = 'pen';
  81. // touch event defines
  82. Hammer.EVENT_START = 'start';
  83. Hammer.EVENT_MOVE = 'move';
  84. Hammer.EVENT_END = 'end';
  85. // hammer document where the base events are added at
  86. Hammer.DOCUMENT = document;
  87. // plugins namespace
  88. Hammer.plugins = {};
  89. // if the window events are set...
  90. Hammer.READY = false;
  91. /**
  92. * setup events to detect gestures on the document
  93. */
  94. function setup() {
  95. if(Hammer.READY) {
  96. return;
  97. }
  98. // find what eventtypes we add listeners to
  99. Hammer.event.determineEventTypes();
  100. // Register all gestures inside Hammer.gestures
  101. for(var name in Hammer.gestures) {
  102. if(Hammer.gestures.hasOwnProperty(name)) {
  103. Hammer.detection.register(Hammer.gestures[name]);
  104. }
  105. }
  106. // Add touch events on the document
  107. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  108. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  109. // Hammer is ready...!
  110. Hammer.READY = true;
  111. }
  112. /**
  113. * create new hammer instance
  114. * all methods should return the instance itself, so it is chainable.
  115. * @param {HTMLElement} element
  116. * @param {Object} [options={}]
  117. * @returns {Hammer.Instance}
  118. * @constructor
  119. */
  120. Hammer.Instance = function(element, options) {
  121. var self = this;
  122. // setup HammerJS window events and register all gestures
  123. // this also sets up the default options
  124. setup();
  125. this.element = element;
  126. // start/stop detection option
  127. this.enabled = true;
  128. // merge options
  129. this.options = Hammer.utils.extend(
  130. Hammer.utils.extend({}, Hammer.defaults),
  131. options || {});
  132. // add some css to the element to prevent the browser from doing its native behavoir
  133. if(this.options.stop_browser_behavior) {
  134. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  135. }
  136. // start detection on touchstart
  137. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  138. if(self.enabled) {
  139. Hammer.detection.startDetect(self, ev);
  140. }
  141. });
  142. // return instance
  143. return this;
  144. };
  145. Hammer.Instance.prototype = {
  146. /**
  147. * bind events to the instance
  148. * @param {String} gesture
  149. * @param {Function} handler
  150. * @returns {Hammer.Instance}
  151. */
  152. on: function onEvent(gesture, handler){
  153. var gestures = gesture.split(' ');
  154. for(var t=0; t<gestures.length; t++) {
  155. this.element.addEventListener(gestures[t], handler, false);
  156. }
  157. return this;
  158. },
  159. /**
  160. * unbind events to the instance
  161. * @param {String} gesture
  162. * @param {Function} handler
  163. * @returns {Hammer.Instance}
  164. */
  165. off: function offEvent(gesture, handler){
  166. var gestures = gesture.split(' ');
  167. for(var t=0; t<gestures.length; t++) {
  168. this.element.removeEventListener(gestures[t], handler, false);
  169. }
  170. return this;
  171. },
  172. /**
  173. * trigger gesture event
  174. * @param {String} gesture
  175. * @param {Object} eventData
  176. * @returns {Hammer.Instance}
  177. */
  178. trigger: function triggerEvent(gesture, eventData){
  179. // create DOM event
  180. var event = Hammer.DOCUMENT.createEvent('Event');
  181. event.initEvent(gesture, true, true);
  182. event.gesture = eventData;
  183. // trigger on the target if it is in the instance element,
  184. // this is for event delegation tricks
  185. var element = this.element;
  186. if(Hammer.utils.hasParent(eventData.target, element)) {
  187. element = eventData.target;
  188. }
  189. element.dispatchEvent(event);
  190. return this;
  191. },
  192. /**
  193. * enable of disable hammer.js detection
  194. * @param {Boolean} state
  195. * @returns {Hammer.Instance}
  196. */
  197. enable: function enable(state) {
  198. this.enabled = state;
  199. return this;
  200. }
  201. };
  202. /**
  203. * this holds the last move event,
  204. * used to fix empty touchend issue
  205. * see the onTouch event for an explanation
  206. * @type {Object}
  207. */
  208. var last_move_event = null;
  209. /**
  210. * when the mouse is hold down, this is true
  211. * @type {Boolean}
  212. */
  213. var enable_detect = false;
  214. /**
  215. * when touch events have been fired, this is true
  216. * @type {Boolean}
  217. */
  218. var touch_triggered = false;
  219. Hammer.event = {
  220. /**
  221. * simple addEventListener
  222. * @param {HTMLElement} element
  223. * @param {String} type
  224. * @param {Function} handler
  225. */
  226. bindDom: function(element, type, handler) {
  227. var types = type.split(' ');
  228. for(var t=0; t<types.length; t++) {
  229. element.addEventListener(types[t], handler, false);
  230. }
  231. },
  232. /**
  233. * touch events with mouse fallback
  234. * @param {HTMLElement} element
  235. * @param {String} eventType like Hammer.EVENT_MOVE
  236. * @param {Function} handler
  237. */
  238. onTouch: function onTouch(element, eventType, handler) {
  239. var self = this;
  240. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  241. var sourceEventType = ev.type.toLowerCase();
  242. // onmouseup, but when touchend has been fired we do nothing.
  243. // this is for touchdevices which also fire a mouseup on touchend
  244. if(sourceEventType.match(/mouse/) && touch_triggered) {
  245. return;
  246. }
  247. // mousebutton must be down or a touch event
  248. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  249. sourceEventType.match(/pointerdown/) || // pointerevents touch
  250. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  251. ){
  252. enable_detect = true;
  253. }
  254. // we are in a touch event, set the touch triggered bool to true,
  255. // this for the conflicts that may occur on ios and android
  256. if(sourceEventType.match(/touch|pointer/)) {
  257. touch_triggered = true;
  258. }
  259. // count the total touches on the screen
  260. var count_touches = 0;
  261. // when touch has been triggered in this detection session
  262. // and we are now handling a mouse event, we stop that to prevent conflicts
  263. if(enable_detect) {
  264. // update pointerevent
  265. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  266. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  267. }
  268. // touch
  269. else if(sourceEventType.match(/touch/)) {
  270. count_touches = ev.touches.length;
  271. }
  272. // mouse
  273. else if(!touch_triggered) {
  274. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  275. }
  276. // if we are in a end event, but when we remove one touch and
  277. // we still have enough, set eventType to move
  278. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  279. eventType = Hammer.EVENT_MOVE;
  280. }
  281. // no touches, force the end event
  282. else if(!count_touches) {
  283. eventType = Hammer.EVENT_END;
  284. }
  285. // because touchend has no touches, and we often want to use these in our gestures,
  286. // we send the last move event as our eventData in touchend
  287. if(!count_touches && last_move_event !== null) {
  288. ev = last_move_event;
  289. }
  290. // store the last move event
  291. else {
  292. last_move_event = ev;
  293. }
  294. // trigger the handler
  295. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  296. // remove pointerevent from list
  297. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  298. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  299. }
  300. }
  301. //debug(sourceEventType +" "+ eventType);
  302. // on the end we reset everything
  303. if(!count_touches) {
  304. last_move_event = null;
  305. enable_detect = false;
  306. touch_triggered = false;
  307. Hammer.PointerEvent.reset();
  308. }
  309. });
  310. },
  311. /**
  312. * we have different events for each device/browser
  313. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  314. */
  315. determineEventTypes: function determineEventTypes() {
  316. // determine the eventtype we want to set
  317. var types;
  318. // pointerEvents magic
  319. if(Hammer.HAS_POINTEREVENTS) {
  320. types = Hammer.PointerEvent.getEvents();
  321. }
  322. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  323. else if(Hammer.NO_MOUSEEVENTS) {
  324. types = [
  325. 'touchstart',
  326. 'touchmove',
  327. 'touchend touchcancel'];
  328. }
  329. // for non pointer events browsers and mixed browsers,
  330. // like chrome on windows8 touch laptop
  331. else {
  332. types = [
  333. 'touchstart mousedown',
  334. 'touchmove mousemove',
  335. 'touchend touchcancel mouseup'];
  336. }
  337. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  338. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  339. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  340. },
  341. /**
  342. * create touchlist depending on the event
  343. * @param {Object} ev
  344. * @param {String} eventType used by the fakemultitouch plugin
  345. */
  346. getTouchList: function getTouchList(ev/*, eventType*/) {
  347. // get the fake pointerEvent touchlist
  348. if(Hammer.HAS_POINTEREVENTS) {
  349. return Hammer.PointerEvent.getTouchList();
  350. }
  351. // get the touchlist
  352. else if(ev.touches) {
  353. return ev.touches;
  354. }
  355. // make fake touchlist from mouse position
  356. else {
  357. return [{
  358. identifier: 1,
  359. pageX: ev.pageX,
  360. pageY: ev.pageY,
  361. target: ev.target
  362. }];
  363. }
  364. },
  365. /**
  366. * collect event data for Hammer js
  367. * @param {HTMLElement} element
  368. * @param {String} eventType like Hammer.EVENT_MOVE
  369. * @param {Object} eventData
  370. */
  371. collectEventData: function collectEventData(element, eventType, ev) {
  372. var touches = this.getTouchList(ev, eventType);
  373. // find out pointerType
  374. var pointerType = Hammer.POINTER_TOUCH;
  375. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  376. pointerType = Hammer.POINTER_MOUSE;
  377. }
  378. return {
  379. center : Hammer.utils.getCenter(touches),
  380. timeStamp : new Date().getTime(),
  381. target : ev.target,
  382. touches : touches,
  383. eventType : eventType,
  384. pointerType : pointerType,
  385. srcEvent : ev,
  386. /**
  387. * prevent the browser default actions
  388. * mostly used to disable scrolling of the browser
  389. */
  390. preventDefault: function() {
  391. if(this.srcEvent.preventManipulation) {
  392. this.srcEvent.preventManipulation();
  393. }
  394. if(this.srcEvent.preventDefault) {
  395. this.srcEvent.preventDefault();
  396. }
  397. },
  398. /**
  399. * stop bubbling the event up to its parents
  400. */
  401. stopPropagation: function() {
  402. this.srcEvent.stopPropagation();
  403. },
  404. /**
  405. * immediately stop gesture detection
  406. * might be useful after a swipe was detected
  407. * @return {*}
  408. */
  409. stopDetect: function() {
  410. return Hammer.detection.stopDetect();
  411. }
  412. };
  413. }
  414. };
  415. Hammer.PointerEvent = {
  416. /**
  417. * holds all pointers
  418. * @type {Object}
  419. */
  420. pointers: {},
  421. /**
  422. * get a list of pointers
  423. * @returns {Array} touchlist
  424. */
  425. getTouchList: function() {
  426. var self = this;
  427. var touchlist = [];
  428. // we can use forEach since pointerEvents only is in IE10
  429. Object.keys(self.pointers).sort().forEach(function(id) {
  430. touchlist.push(self.pointers[id]);
  431. });
  432. return touchlist;
  433. },
  434. /**
  435. * update the position of a pointer
  436. * @param {String} type Hammer.EVENT_END
  437. * @param {Object} pointerEvent
  438. */
  439. updatePointer: function(type, pointerEvent) {
  440. if(type == Hammer.EVENT_END) {
  441. this.pointers = {};
  442. }
  443. else {
  444. pointerEvent.identifier = pointerEvent.pointerId;
  445. this.pointers[pointerEvent.pointerId] = pointerEvent;
  446. }
  447. return Object.keys(this.pointers).length;
  448. },
  449. /**
  450. * check if ev matches pointertype
  451. * @param {String} pointerType Hammer.POINTER_MOUSE
  452. * @param {PointerEvent} ev
  453. */
  454. matchType: function(pointerType, ev) {
  455. if(!ev.pointerType) {
  456. return false;
  457. }
  458. var types = {};
  459. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  460. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  461. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  462. return types[pointerType];
  463. },
  464. /**
  465. * get events
  466. */
  467. getEvents: function() {
  468. return [
  469. 'pointerdown MSPointerDown',
  470. 'pointermove MSPointerMove',
  471. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  472. ];
  473. },
  474. /**
  475. * reset the list
  476. */
  477. reset: function() {
  478. this.pointers = {};
  479. }
  480. };
  481. Hammer.utils = {
  482. /**
  483. * extend method,
  484. * also used for cloning when dest is an empty object
  485. * @param {Object} dest
  486. * @param {Object} src
  487. * @parm {Boolean} merge do a merge
  488. * @returns {Object} dest
  489. */
  490. extend: function extend(dest, src, merge) {
  491. for (var key in src) {
  492. if(dest[key] !== undefined && merge) {
  493. continue;
  494. }
  495. dest[key] = src[key];
  496. }
  497. return dest;
  498. },
  499. /**
  500. * find if a node is in the given parent
  501. * used for event delegation tricks
  502. * @param {HTMLElement} node
  503. * @param {HTMLElement} parent
  504. * @returns {boolean} has_parent
  505. */
  506. hasParent: function(node, parent) {
  507. while(node){
  508. if(node == parent) {
  509. return true;
  510. }
  511. node = node.parentNode;
  512. }
  513. return false;
  514. },
  515. /**
  516. * get the center of all the touches
  517. * @param {Array} touches
  518. * @returns {Object} center
  519. */
  520. getCenter: function getCenter(touches) {
  521. var valuesX = [], valuesY = [];
  522. for(var t= 0,len=touches.length; t<len; t++) {
  523. valuesX.push(touches[t].pageX);
  524. valuesY.push(touches[t].pageY);
  525. }
  526. return {
  527. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  528. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  529. };
  530. },
  531. /**
  532. * calculate the velocity between two points
  533. * @param {Number} delta_time
  534. * @param {Number} delta_x
  535. * @param {Number} delta_y
  536. * @returns {Object} velocity
  537. */
  538. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  539. return {
  540. x: Math.abs(delta_x / delta_time) || 0,
  541. y: Math.abs(delta_y / delta_time) || 0
  542. };
  543. },
  544. /**
  545. * calculate the angle between two coordinates
  546. * @param {Touch} touch1
  547. * @param {Touch} touch2
  548. * @returns {Number} angle
  549. */
  550. getAngle: function getAngle(touch1, touch2) {
  551. var y = touch2.pageY - touch1.pageY,
  552. x = touch2.pageX - touch1.pageX;
  553. return Math.atan2(y, x) * 180 / Math.PI;
  554. },
  555. /**
  556. * angle to direction define
  557. * @param {Touch} touch1
  558. * @param {Touch} touch2
  559. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  560. */
  561. getDirection: function getDirection(touch1, touch2) {
  562. var x = Math.abs(touch1.pageX - touch2.pageX),
  563. y = Math.abs(touch1.pageY - touch2.pageY);
  564. if(x >= y) {
  565. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  566. }
  567. else {
  568. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  569. }
  570. },
  571. /**
  572. * calculate the distance between two touches
  573. * @param {Touch} touch1
  574. * @param {Touch} touch2
  575. * @returns {Number} distance
  576. */
  577. getDistance: function getDistance(touch1, touch2) {
  578. var x = touch2.pageX - touch1.pageX,
  579. y = touch2.pageY - touch1.pageY;
  580. return Math.sqrt((x*x) + (y*y));
  581. },
  582. /**
  583. * calculate the scale factor between two touchLists (fingers)
  584. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  585. * @param {Array} start
  586. * @param {Array} end
  587. * @returns {Number} scale
  588. */
  589. getScale: function getScale(start, end) {
  590. // need two fingers...
  591. if(start.length >= 2 && end.length >= 2) {
  592. return this.getDistance(end[0], end[1]) /
  593. this.getDistance(start[0], start[1]);
  594. }
  595. return 1;
  596. },
  597. /**
  598. * calculate the rotation degrees between two touchLists (fingers)
  599. * @param {Array} start
  600. * @param {Array} end
  601. * @returns {Number} rotation
  602. */
  603. getRotation: function getRotation(start, end) {
  604. // need two fingers
  605. if(start.length >= 2 && end.length >= 2) {
  606. return this.getAngle(end[1], end[0]) -
  607. this.getAngle(start[1], start[0]);
  608. }
  609. return 0;
  610. },
  611. /**
  612. * boolean if the direction is vertical
  613. * @param {String} direction
  614. * @returns {Boolean} is_vertical
  615. */
  616. isVertical: function isVertical(direction) {
  617. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  618. },
  619. /**
  620. * stop browser default behavior with css props
  621. * @param {HtmlElement} element
  622. * @param {Object} css_props
  623. */
  624. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  625. var prop,
  626. vendors = ['webkit','khtml','moz','ms','o',''];
  627. if(!css_props || !element.style) {
  628. return;
  629. }
  630. // with css properties for modern browsers
  631. for(var i = 0; i < vendors.length; i++) {
  632. for(var p in css_props) {
  633. if(css_props.hasOwnProperty(p)) {
  634. prop = p;
  635. // vender prefix at the property
  636. if(vendors[i]) {
  637. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  638. }
  639. // set the style
  640. element.style[prop] = css_props[p];
  641. }
  642. }
  643. }
  644. // also the disable onselectstart
  645. if(css_props.userSelect == 'none') {
  646. element.onselectstart = function() {
  647. return false;
  648. };
  649. }
  650. }
  651. };
  652. Hammer.detection = {
  653. // contains all registred Hammer.gestures in the correct order
  654. gestures: [],
  655. // data of the current Hammer.gesture detection session
  656. current: null,
  657. // the previous Hammer.gesture session data
  658. // is a full clone of the previous gesture.current object
  659. previous: null,
  660. // when this becomes true, no gestures are fired
  661. stopped: false,
  662. /**
  663. * start Hammer.gesture detection
  664. * @param {Hammer.Instance} inst
  665. * @param {Object} eventData
  666. */
  667. startDetect: function startDetect(inst, eventData) {
  668. // already busy with a Hammer.gesture detection on an element
  669. if(this.current) {
  670. return;
  671. }
  672. this.stopped = false;
  673. this.current = {
  674. inst : inst, // reference to HammerInstance we're working for
  675. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  676. lastEvent : false, // last eventData
  677. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  678. };
  679. this.detect(eventData);
  680. },
  681. /**
  682. * Hammer.gesture detection
  683. * @param {Object} eventData
  684. * @param {Object} eventData
  685. */
  686. detect: function detect(eventData) {
  687. if(!this.current || this.stopped) {
  688. return;
  689. }
  690. // extend event data with calculations about scale, distance etc
  691. eventData = this.extendEventData(eventData);
  692. // instance options
  693. var inst_options = this.current.inst.options;
  694. // call Hammer.gesture handlers
  695. for(var g=0,len=this.gestures.length; g<len; g++) {
  696. var gesture = this.gestures[g];
  697. // only when the instance options have enabled this gesture
  698. if(!this.stopped && inst_options[gesture.name] !== false) {
  699. // if a handler returns false, we stop with the detection
  700. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  701. this.stopDetect();
  702. break;
  703. }
  704. }
  705. }
  706. // store as previous event event
  707. if(this.current) {
  708. this.current.lastEvent = eventData;
  709. }
  710. // endevent, but not the last touch, so dont stop
  711. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  712. this.stopDetect();
  713. }
  714. return eventData;
  715. },
  716. /**
  717. * clear the Hammer.gesture vars
  718. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  719. * to stop other Hammer.gestures from being fired
  720. */
  721. stopDetect: function stopDetect() {
  722. // clone current data to the store as the previous gesture
  723. // used for the double tap gesture, since this is an other gesture detect session
  724. this.previous = Hammer.utils.extend({}, this.current);
  725. // reset the current
  726. this.current = null;
  727. // stopped!
  728. this.stopped = true;
  729. },
  730. /**
  731. * extend eventData for Hammer.gestures
  732. * @param {Object} ev
  733. * @returns {Object} ev
  734. */
  735. extendEventData: function extendEventData(ev) {
  736. var startEv = this.current.startEvent;
  737. // if the touches change, set the new touches over the startEvent touches
  738. // this because touchevents don't have all the touches on touchstart, or the
  739. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  740. // but, sometimes it happens that both fingers are touching at the EXACT same time
  741. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  742. // extend 1 level deep to get the touchlist with the touch objects
  743. startEv.touches = [];
  744. for(var i=0,len=ev.touches.length; i<len; i++) {
  745. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  746. }
  747. }
  748. var delta_time = ev.timeStamp - startEv.timeStamp,
  749. delta_x = ev.center.pageX - startEv.center.pageX,
  750. delta_y = ev.center.pageY - startEv.center.pageY,
  751. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  752. Hammer.utils.extend(ev, {
  753. deltaTime : delta_time,
  754. deltaX : delta_x,
  755. deltaY : delta_y,
  756. velocityX : velocity.x,
  757. velocityY : velocity.y,
  758. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  759. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  760. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  761. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  762. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  763. startEvent : startEv
  764. });
  765. return ev;
  766. },
  767. /**
  768. * register new gesture
  769. * @param {Object} gesture object, see gestures.js for documentation
  770. * @returns {Array} gestures
  771. */
  772. register: function register(gesture) {
  773. // add an enable gesture options if there is no given
  774. var options = gesture.defaults || {};
  775. if(options[gesture.name] === undefined) {
  776. options[gesture.name] = true;
  777. }
  778. // extend Hammer default options with the Hammer.gesture options
  779. Hammer.utils.extend(Hammer.defaults, options, true);
  780. // set its index
  781. gesture.index = gesture.index || 1000;
  782. // add Hammer.gesture to the list
  783. this.gestures.push(gesture);
  784. // sort the list by index
  785. this.gestures.sort(function(a, b) {
  786. if (a.index < b.index) {
  787. return -1;
  788. }
  789. if (a.index > b.index) {
  790. return 1;
  791. }
  792. return 0;
  793. });
  794. return this.gestures;
  795. }
  796. };
  797. Hammer.gestures = Hammer.gestures || {};
  798. /**
  799. * Custom gestures
  800. * ==============================
  801. *
  802. * Gesture object
  803. * --------------------
  804. * The object structure of a gesture:
  805. *
  806. * { name: 'mygesture',
  807. * index: 1337,
  808. * defaults: {
  809. * mygesture_option: true
  810. * }
  811. * handler: function(type, ev, inst) {
  812. * // trigger gesture event
  813. * inst.trigger(this.name, ev);
  814. * }
  815. * }
  816. * @param {String} name
  817. * this should be the name of the gesture, lowercase
  818. * it is also being used to disable/enable the gesture per instance config.
  819. *
  820. * @param {Number} [index=1000]
  821. * the index of the gesture, where it is going to be in the stack of gestures detection
  822. * like when you build an gesture that depends on the drag gesture, it is a good
  823. * idea to place it after the index of the drag gesture.
  824. *
  825. * @param {Object} [defaults={}]
  826. * the default settings of the gesture. these are added to the instance settings,
  827. * and can be overruled per instance. you can also add the name of the gesture,
  828. * but this is also added by default (and set to true).
  829. *
  830. * @param {Function} handler
  831. * this handles the gesture detection of your custom gesture and receives the
  832. * following arguments:
  833. *
  834. * @param {Object} eventData
  835. * event data containing the following properties:
  836. * timeStamp {Number} time the event occurred
  837. * target {HTMLElement} target element
  838. * touches {Array} touches (fingers, pointers, mouse) on the screen
  839. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  840. * center {Object} center position of the touches. contains pageX and pageY
  841. * deltaTime {Number} the total time of the touches in the screen
  842. * deltaX {Number} the delta on x axis we haved moved
  843. * deltaY {Number} the delta on y axis we haved moved
  844. * velocityX {Number} the velocity on the x
  845. * velocityY {Number} the velocity on y
  846. * angle {Number} the angle we are moving
  847. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  848. * distance {Number} the distance we haved moved
  849. * scale {Number} scaling of the touches, needs 2 touches
  850. * rotation {Number} rotation of the touches, needs 2 touches *
  851. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  852. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  853. * startEvent {Object} contains the same properties as above,
  854. * but from the first touch. this is used to calculate
  855. * distances, deltaTime, scaling etc
  856. *
  857. * @param {Hammer.Instance} inst
  858. * the instance we are doing the detection for. you can get the options from
  859. * the inst.options object and trigger the gesture event by calling inst.trigger
  860. *
  861. *
  862. * Handle gestures
  863. * --------------------
  864. * inside the handler you can get/set Hammer.detection.current. This is the current
  865. * detection session. It has the following properties
  866. * @param {String} name
  867. * contains the name of the gesture we have detected. it has not a real function,
  868. * only to check in other gestures if something is detected.
  869. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  870. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  871. *
  872. * @readonly
  873. * @param {Hammer.Instance} inst
  874. * the instance we do the detection for
  875. *
  876. * @readonly
  877. * @param {Object} startEvent
  878. * contains the properties of the first gesture detection in this session.
  879. * Used for calculations about timing, distance, etc.
  880. *
  881. * @readonly
  882. * @param {Object} lastEvent
  883. * contains all the properties of the last gesture detect in this session.
  884. *
  885. * after the gesture detection session has been completed (user has released the screen)
  886. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  887. * this is usefull for gestures like doubletap, where you need to know if the
  888. * previous gesture was a tap
  889. *
  890. * options that have been set by the instance can be received by calling inst.options
  891. *
  892. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  893. * The first param is the name of your gesture, the second the event argument
  894. *
  895. *
  896. * Register gestures
  897. * --------------------
  898. * When an gesture is added to the Hammer.gestures object, it is auto registered
  899. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  900. * manually and pass your gesture object as a param
  901. *
  902. */
  903. /**
  904. * Hold
  905. * Touch stays at the same place for x time
  906. * @events hold
  907. */
  908. Hammer.gestures.Hold = {
  909. name: 'hold',
  910. index: 10,
  911. defaults: {
  912. hold_timeout : 500,
  913. hold_threshold : 1
  914. },
  915. timer: null,
  916. handler: function holdGesture(ev, inst) {
  917. switch(ev.eventType) {
  918. case Hammer.EVENT_START:
  919. // clear any running timers
  920. clearTimeout(this.timer);
  921. // set the gesture so we can check in the timeout if it still is
  922. Hammer.detection.current.name = this.name;
  923. // set timer and if after the timeout it still is hold,
  924. // we trigger the hold event
  925. this.timer = setTimeout(function() {
  926. if(Hammer.detection.current.name == 'hold') {
  927. inst.trigger('hold', ev);
  928. }
  929. }, inst.options.hold_timeout);
  930. break;
  931. // when you move or end we clear the timer
  932. case Hammer.EVENT_MOVE:
  933. if(ev.distance > inst.options.hold_threshold) {
  934. clearTimeout(this.timer);
  935. }
  936. break;
  937. case Hammer.EVENT_END:
  938. clearTimeout(this.timer);
  939. break;
  940. }
  941. }
  942. };
  943. /**
  944. * Tap/DoubleTap
  945. * Quick touch at a place or double at the same place
  946. * @events tap, doubletap
  947. */
  948. Hammer.gestures.Tap = {
  949. name: 'tap',
  950. index: 100,
  951. defaults: {
  952. tap_max_touchtime : 250,
  953. tap_max_distance : 10,
  954. tap_always : true,
  955. doubletap_distance : 20,
  956. doubletap_interval : 300
  957. },
  958. handler: function tapGesture(ev, inst) {
  959. if(ev.eventType == Hammer.EVENT_END) {
  960. // previous gesture, for the double tap since these are two different gesture detections
  961. var prev = Hammer.detection.previous,
  962. did_doubletap = false;
  963. // when the touchtime is higher then the max touch time
  964. // or when the moving distance is too much
  965. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  966. ev.distance > inst.options.tap_max_distance) {
  967. return;
  968. }
  969. // check if double tap
  970. if(prev && prev.name == 'tap' &&
  971. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  972. ev.distance < inst.options.doubletap_distance) {
  973. inst.trigger('doubletap', ev);
  974. did_doubletap = true;
  975. }
  976. // do a single tap
  977. if(!did_doubletap || inst.options.tap_always) {
  978. Hammer.detection.current.name = 'tap';
  979. inst.trigger(Hammer.detection.current.name, ev);
  980. }
  981. }
  982. }
  983. };
  984. /**
  985. * Swipe
  986. * triggers swipe events when the end velocity is above the threshold
  987. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  988. */
  989. Hammer.gestures.Swipe = {
  990. name: 'swipe',
  991. index: 40,
  992. defaults: {
  993. // set 0 for unlimited, but this can conflict with transform
  994. swipe_max_touches : 1,
  995. swipe_velocity : 0.7
  996. },
  997. handler: function swipeGesture(ev, inst) {
  998. if(ev.eventType == Hammer.EVENT_END) {
  999. // max touches
  1000. if(inst.options.swipe_max_touches > 0 &&
  1001. ev.touches.length > inst.options.swipe_max_touches) {
  1002. return;
  1003. }
  1004. // when the distance we moved is too small we skip this gesture
  1005. // or we can be already in dragging
  1006. if(ev.velocityX > inst.options.swipe_velocity ||
  1007. ev.velocityY > inst.options.swipe_velocity) {
  1008. // trigger swipe events
  1009. inst.trigger(this.name, ev);
  1010. inst.trigger(this.name + ev.direction, ev);
  1011. }
  1012. }
  1013. }
  1014. };
  1015. /**
  1016. * Drag
  1017. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  1018. * moving left and right is a good practice. When all the drag events are blocking
  1019. * you disable scrolling on that area.
  1020. * @events drag, drapleft, dragright, dragup, dragdown
  1021. */
  1022. Hammer.gestures.Drag = {
  1023. name: 'drag',
  1024. index: 50,
  1025. defaults: {
  1026. drag_min_distance : 10,
  1027. // set 0 for unlimited, but this can conflict with transform
  1028. drag_max_touches : 1,
  1029. // prevent default browser behavior when dragging occurs
  1030. // be careful with it, it makes the element a blocking element
  1031. // when you are using the drag gesture, it is a good practice to set this true
  1032. drag_block_horizontal : false,
  1033. drag_block_vertical : false,
  1034. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  1035. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  1036. drag_lock_to_axis : false,
  1037. // drag lock only kicks in when distance > drag_lock_min_distance
  1038. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  1039. drag_lock_min_distance : 25
  1040. },
  1041. triggered: false,
  1042. handler: function dragGesture(ev, inst) {
  1043. // current gesture isnt drag, but dragged is true
  1044. // this means an other gesture is busy. now call dragend
  1045. if(Hammer.detection.current.name != this.name && this.triggered) {
  1046. inst.trigger(this.name +'end', ev);
  1047. this.triggered = false;
  1048. return;
  1049. }
  1050. // max touches
  1051. if(inst.options.drag_max_touches > 0 &&
  1052. ev.touches.length > inst.options.drag_max_touches) {
  1053. return;
  1054. }
  1055. switch(ev.eventType) {
  1056. case Hammer.EVENT_START:
  1057. this.triggered = false;
  1058. break;
  1059. case Hammer.EVENT_MOVE:
  1060. // when the distance we moved is too small we skip this gesture
  1061. // or we can be already in dragging
  1062. if(ev.distance < inst.options.drag_min_distance &&
  1063. Hammer.detection.current.name != this.name) {
  1064. return;
  1065. }
  1066. // we are dragging!
  1067. Hammer.detection.current.name = this.name;
  1068. // lock drag to axis?
  1069. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  1070. ev.drag_locked_to_axis = true;
  1071. }
  1072. var last_direction = Hammer.detection.current.lastEvent.direction;
  1073. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  1074. // keep direction on the axis that the drag gesture started on
  1075. if(Hammer.utils.isVertical(last_direction)) {
  1076. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  1077. }
  1078. else {
  1079. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  1080. }
  1081. }
  1082. // first time, trigger dragstart event
  1083. if(!this.triggered) {
  1084. inst.trigger(this.name +'start', ev);
  1085. this.triggered = true;
  1086. }
  1087. // trigger normal event
  1088. inst.trigger(this.name, ev);
  1089. // direction event, like dragdown
  1090. inst.trigger(this.name + ev.direction, ev);
  1091. // block the browser events
  1092. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  1093. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  1094. ev.preventDefault();
  1095. }
  1096. break;
  1097. case Hammer.EVENT_END:
  1098. // trigger dragend
  1099. if(this.triggered) {
  1100. inst.trigger(this.name +'end', ev);
  1101. }
  1102. this.triggered = false;
  1103. break;
  1104. }
  1105. }
  1106. };
  1107. /**
  1108. * Transform
  1109. * User want to scale or rotate with 2 fingers
  1110. * @events transform, pinch, pinchin, pinchout, rotate
  1111. */
  1112. Hammer.gestures.Transform = {
  1113. name: 'transform',
  1114. index: 45,
  1115. defaults: {
  1116. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  1117. transform_min_scale : 0.01,
  1118. // rotation in degrees
  1119. transform_min_rotation : 1,
  1120. // prevent default browser behavior when two touches are on the screen
  1121. // but it makes the element a blocking element
  1122. // when you are using the transform gesture, it is a good practice to set this true
  1123. transform_always_block : false
  1124. },
  1125. triggered: false,
  1126. handler: function transformGesture(ev, inst) {
  1127. // current gesture isnt drag, but dragged is true
  1128. // this means an other gesture is busy. now call dragend
  1129. if(Hammer.detection.current.name != this.name && this.triggered) {
  1130. inst.trigger(this.name +'end', ev);
  1131. this.triggered = false;
  1132. return;
  1133. }
  1134. // atleast multitouch
  1135. if(ev.touches.length < 2) {
  1136. return;
  1137. }
  1138. // prevent default when two fingers are on the screen
  1139. if(inst.options.transform_always_block) {
  1140. ev.preventDefault();
  1141. }
  1142. switch(ev.eventType) {
  1143. case Hammer.EVENT_START:
  1144. this.triggered = false;
  1145. break;
  1146. case Hammer.EVENT_MOVE:
  1147. var scale_threshold = Math.abs(1-ev.scale);
  1148. var rotation_threshold = Math.abs(ev.rotation);
  1149. // when the distance we moved is too small we skip this gesture
  1150. // or we can be already in dragging
  1151. if(scale_threshold < inst.options.transform_min_scale &&
  1152. rotation_threshold < inst.options.transform_min_rotation) {
  1153. return;
  1154. }
  1155. // we are transforming!
  1156. Hammer.detection.current.name = this.name;
  1157. // first time, trigger dragstart event
  1158. if(!this.triggered) {
  1159. inst.trigger(this.name +'start', ev);
  1160. this.triggered = true;
  1161. }
  1162. inst.trigger(this.name, ev); // basic transform event
  1163. // trigger rotate event
  1164. if(rotation_threshold > inst.options.transform_min_rotation) {
  1165. inst.trigger('rotate', ev);
  1166. }
  1167. // trigger pinch event
  1168. if(scale_threshold > inst.options.transform_min_scale) {
  1169. inst.trigger('pinch', ev);
  1170. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  1171. }
  1172. break;
  1173. case Hammer.EVENT_END:
  1174. // trigger dragend
  1175. if(this.triggered) {
  1176. inst.trigger(this.name +'end', ev);
  1177. }
  1178. this.triggered = false;
  1179. break;
  1180. }
  1181. }
  1182. };
  1183. /**
  1184. * Touch
  1185. * Called as first, tells the user has touched the screen
  1186. * @events touch
  1187. */
  1188. Hammer.gestures.Touch = {
  1189. name: 'touch',
  1190. index: -Infinity,
  1191. defaults: {
  1192. // call preventDefault at touchstart, and makes the element blocking by
  1193. // disabling the scrolling of the page, but it improves gestures like
  1194. // transforming and dragging.
  1195. // be careful with using this, it can be very annoying for users to be stuck
  1196. // on the page
  1197. prevent_default: false,
  1198. // disable mouse events, so only touch (or pen!) input triggers events
  1199. prevent_mouseevents: false
  1200. },
  1201. handler: function touchGesture(ev, inst) {
  1202. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  1203. ev.stopDetect();
  1204. return;
  1205. }
  1206. if(inst.options.prevent_default) {
  1207. ev.preventDefault();
  1208. }
  1209. if(ev.eventType == Hammer.EVENT_START) {
  1210. inst.trigger(this.name, ev);
  1211. }
  1212. }
  1213. };
  1214. /**
  1215. * Release
  1216. * Called as last, tells the user has released the screen
  1217. * @events release
  1218. */
  1219. Hammer.gestures.Release = {
  1220. name: 'release',
  1221. index: Infinity,
  1222. handler: function releaseGesture(ev, inst) {
  1223. if(ev.eventType == Hammer.EVENT_END) {
  1224. inst.trigger(this.name, ev);
  1225. }
  1226. }
  1227. };
  1228. // node export
  1229. if(typeof module === 'object' && typeof module.exports === 'object'){
  1230. module.exports = Hammer;
  1231. }
  1232. // just window export
  1233. else {
  1234. window.Hammer = Hammer;
  1235. // requireJS module definition
  1236. if(typeof window.define === 'function' && window.define.amd) {
  1237. window.define('hammer', [], function() {
  1238. return Hammer;
  1239. });
  1240. }
  1241. }
  1242. })(this);
  1243. },{}],2:[function(require,module,exports){
  1244. //! moment.js
  1245. //! version : 2.5.0
  1246. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  1247. //! license : MIT
  1248. //! momentjs.com
  1249. (function (undefined) {
  1250. /************************************
  1251. Constants
  1252. ************************************/
  1253. var moment,
  1254. VERSION = "2.5.0",
  1255. global = this,
  1256. round = Math.round,
  1257. i,
  1258. YEAR = 0,
  1259. MONTH = 1,
  1260. DATE = 2,
  1261. HOUR = 3,
  1262. MINUTE = 4,
  1263. SECOND = 5,
  1264. MILLISECOND = 6,
  1265. // internal storage for language config files
  1266. languages = {},
  1267. // check for nodeJS
  1268. hasModule = (typeof module !== 'undefined' && module.exports && typeof require !== 'undefined'),
  1269. // ASP.NET json date format regex
  1270. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  1271. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,
  1272. // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html
  1273. // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere
  1274. isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,
  1275. // format tokens
  1276. 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,
  1277. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  1278. // parsing token regexes
  1279. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  1280. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  1281. parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999
  1282. parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  1283. parseTokenDigits = /\d+/, // nonzero number of digits
  1284. 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.
  1285. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z
  1286. parseTokenT = /T/i, // T (ISO separator)
  1287. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  1288. //strict parsing regexes
  1289. parseTokenOneDigit = /\d/, // 0 - 9
  1290. parseTokenTwoDigits = /\d\d/, // 00 - 99
  1291. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  1292. parseTokenFourDigits = /\d{4}/, // 0000 - 9999
  1293. parseTokenSixDigits = /[+\-]?\d{6}/, // -999,999 - 999,999
  1294. // iso 8601 regex
  1295. // 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)
  1296. 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)?)?$/,
  1297. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  1298. isoDates = [
  1299. 'YYYY-MM-DD',
  1300. 'GGGG-[W]WW',
  1301. 'GGGG-[W]WW-E',
  1302. 'YYYY-DDD'
  1303. ],
  1304. // iso time formats and regexes
  1305. isoTimes = [
  1306. ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  1307. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  1308. ['HH:mm', /(T| )\d\d:\d\d/],
  1309. ['HH', /(T| )\d\d/]
  1310. ],
  1311. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  1312. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  1313. // getter and setter names
  1314. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  1315. unitMillisecondFactors = {
  1316. 'Milliseconds' : 1,
  1317. 'Seconds' : 1e3,
  1318. 'Minutes' : 6e4,
  1319. 'Hours' : 36e5,
  1320. 'Days' : 864e5,
  1321. 'Months' : 2592e6,
  1322. 'Years' : 31536e6
  1323. },
  1324. unitAliases = {
  1325. ms : 'millisecond',
  1326. s : 'second',
  1327. m : 'minute',
  1328. h : 'hour',
  1329. d : 'day',
  1330. D : 'date',
  1331. w : 'week',
  1332. W : 'isoWeek',
  1333. M : 'month',
  1334. y : 'year',
  1335. DDD : 'dayOfYear',
  1336. e : 'weekday',
  1337. E : 'isoWeekday',
  1338. gg: 'weekYear',
  1339. GG: 'isoWeekYear'
  1340. },
  1341. camelFunctions = {
  1342. dayofyear : 'dayOfYear',
  1343. isoweekday : 'isoWeekday',
  1344. isoweek : 'isoWeek',
  1345. weekyear : 'weekYear',
  1346. isoweekyear : 'isoWeekYear'
  1347. },
  1348. // format function strings
  1349. formatFunctions = {},
  1350. // tokens to ordinalize and pad
  1351. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  1352. paddedTokens = 'M D H h m s w W'.split(' '),
  1353. formatTokenFunctions = {
  1354. M : function () {
  1355. return this.month() + 1;
  1356. },
  1357. MMM : function (format) {
  1358. return this.lang().monthsShort(this, format);
  1359. },
  1360. MMMM : function (format) {
  1361. return this.lang().months(this, format);
  1362. },
  1363. D : function () {
  1364. return this.date();
  1365. },
  1366. DDD : function () {
  1367. return this.dayOfYear();
  1368. },
  1369. d : function () {
  1370. return this.day();
  1371. },
  1372. dd : function (format) {
  1373. return this.lang().weekdaysMin(this, format);
  1374. },
  1375. ddd : function (format) {
  1376. return this.lang().weekdaysShort(this, format);
  1377. },
  1378. dddd : function (format) {
  1379. return this.lang().weekdays(this, format);
  1380. },
  1381. w : function () {
  1382. return this.week();
  1383. },
  1384. W : function () {
  1385. return this.isoWeek();
  1386. },
  1387. YY : function () {
  1388. return leftZeroFill(this.year() % 100, 2);
  1389. },
  1390. YYYY : function () {
  1391. return leftZeroFill(this.year(), 4);
  1392. },
  1393. YYYYY : function () {
  1394. return leftZeroFill(this.year(), 5);
  1395. },
  1396. YYYYYY : function () {
  1397. var y = this.year(), sign = y >= 0 ? '+' : '-';
  1398. return sign + leftZeroFill(Math.abs(y), 6);
  1399. },
  1400. gg : function () {
  1401. return leftZeroFill(this.weekYear() % 100, 2);
  1402. },
  1403. gggg : function () {
  1404. return this.weekYear();
  1405. },
  1406. ggggg : function () {
  1407. return leftZeroFill(this.weekYear(), 5);
  1408. },
  1409. GG : function () {
  1410. return leftZeroFill(this.isoWeekYear() % 100, 2);
  1411. },
  1412. GGGG : function () {
  1413. return this.isoWeekYear();
  1414. },
  1415. GGGGG : function () {
  1416. return leftZeroFill(this.isoWeekYear(), 5);
  1417. },
  1418. e : function () {
  1419. return this.weekday();
  1420. },
  1421. E : function () {
  1422. return this.isoWeekday();
  1423. },
  1424. a : function () {
  1425. return this.lang().meridiem(this.hours(), this.minutes(), true);
  1426. },
  1427. A : function () {
  1428. return this.lang().meridiem(this.hours(), this.minutes(), false);
  1429. },
  1430. H : function () {
  1431. return this.hours();
  1432. },
  1433. h : function () {
  1434. return this.hours() % 12 || 12;
  1435. },
  1436. m : function () {
  1437. return this.minutes();
  1438. },
  1439. s : function () {
  1440. return this.seconds();
  1441. },
  1442. S : function () {
  1443. return toInt(this.milliseconds() / 100);
  1444. },
  1445. SS : function () {
  1446. return leftZeroFill(toInt(this.milliseconds() / 10), 2);
  1447. },
  1448. SSS : function () {
  1449. return leftZeroFill(this.milliseconds(), 3);
  1450. },
  1451. SSSS : function () {
  1452. return leftZeroFill(this.milliseconds(), 3);
  1453. },
  1454. Z : function () {
  1455. var a = -this.zone(),
  1456. b = "+";
  1457. if (a < 0) {
  1458. a = -a;
  1459. b = "-";
  1460. }
  1461. return b + leftZeroFill(toInt(a / 60), 2) + ":" + leftZeroFill(toInt(a) % 60, 2);
  1462. },
  1463. ZZ : function () {
  1464. var a = -this.zone(),
  1465. b = "+";
  1466. if (a < 0) {
  1467. a = -a;
  1468. b = "-";
  1469. }
  1470. return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2);
  1471. },
  1472. z : function () {
  1473. return this.zoneAbbr();
  1474. },
  1475. zz : function () {
  1476. return this.zoneName();
  1477. },
  1478. X : function () {
  1479. return this.unix();
  1480. },
  1481. Q : function () {
  1482. return this.quarter();
  1483. }
  1484. },
  1485. lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin'];
  1486. function padToken(func, count) {
  1487. return function (a) {
  1488. return leftZeroFill(func.call(this, a), count);
  1489. };
  1490. }
  1491. function ordinalizeToken(func, period) {
  1492. return function (a) {
  1493. return this.lang().ordinal(func.call(this, a), period);
  1494. };
  1495. }
  1496. while (ordinalizeTokens.length) {
  1497. i = ordinalizeTokens.pop();
  1498. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  1499. }
  1500. while (paddedTokens.length) {
  1501. i = paddedTokens.pop();
  1502. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  1503. }
  1504. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  1505. /************************************
  1506. Constructors
  1507. ************************************/
  1508. function Language() {
  1509. }
  1510. // Moment prototype object
  1511. function Moment(config) {
  1512. checkOverflow(config);
  1513. extend(this, config);
  1514. }
  1515. // Duration Constructor
  1516. function Duration(duration) {
  1517. var normalizedInput = normalizeObjectUnits(duration),
  1518. years = normalizedInput.year || 0,
  1519. months = normalizedInput.month || 0,
  1520. weeks = normalizedInput.week || 0,
  1521. days = normalizedInput.day || 0,
  1522. hours = normalizedInput.hour || 0,
  1523. minutes = normalizedInput.minute || 0,
  1524. seconds = normalizedInput.second || 0,
  1525. milliseconds = normalizedInput.millisecond || 0;
  1526. // representation for dateAddRemove
  1527. this._milliseconds = +milliseconds +
  1528. seconds * 1e3 + // 1000
  1529. minutes * 6e4 + // 1000 * 60
  1530. hours * 36e5; // 1000 * 60 * 60
  1531. // Because of dateAddRemove treats 24 hours as different from a
  1532. // day when working around DST, we need to store them separately
  1533. this._days = +days +
  1534. weeks * 7;
  1535. // It is impossible translate months into days without knowing
  1536. // which months you are are talking about, so we have to store
  1537. // it separately.
  1538. this._months = +months +
  1539. years * 12;
  1540. this._data = {};
  1541. this._bubble();
  1542. }
  1543. /************************************
  1544. Helpers
  1545. ************************************/
  1546. function extend(a, b) {
  1547. for (var i in b) {
  1548. if (b.hasOwnProperty(i)) {
  1549. a[i] = b[i];
  1550. }
  1551. }
  1552. if (b.hasOwnProperty("toString")) {
  1553. a.toString = b.toString;
  1554. }
  1555. if (b.hasOwnProperty("valueOf")) {
  1556. a.valueOf = b.valueOf;
  1557. }
  1558. return a;
  1559. }
  1560. function absRound(number) {
  1561. if (number < 0) {
  1562. return Math.ceil(number);
  1563. } else {
  1564. return Math.floor(number);
  1565. }
  1566. }
  1567. // left zero fill a number
  1568. // see http://jsperf.com/left-zero-filling for performance comparison
  1569. function leftZeroFill(number, targetLength, forceSign) {
  1570. var output = Math.abs(number) + '',
  1571. sign = number >= 0;
  1572. while (output.length < targetLength) {
  1573. output = '0' + output;
  1574. }
  1575. return (sign ? (forceSign ? '+' : '') : '-') + output;
  1576. }
  1577. // helper function for _.addTime and _.subtractTime
  1578. function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
  1579. var milliseconds = duration._milliseconds,
  1580. days = duration._days,
  1581. months = duration._months,
  1582. minutes,
  1583. hours;
  1584. if (milliseconds) {
  1585. mom._d.setTime(+mom._d + milliseconds * isAdding);
  1586. }
  1587. // store the minutes and hours so we can restore them
  1588. if (days || months) {
  1589. minutes = mom.minute();
  1590. hours = mom.hour();
  1591. }
  1592. if (days) {
  1593. mom.date(mom.date() + days * isAdding);
  1594. }
  1595. if (months) {
  1596. mom.month(mom.month() + months * isAdding);
  1597. }
  1598. if (milliseconds && !ignoreUpdateOffset) {
  1599. moment.updateOffset(mom);
  1600. }
  1601. // restore the minutes and hours after possibly changing dst
  1602. if (days || months) {
  1603. mom.minute(minutes);
  1604. mom.hour(hours);
  1605. }
  1606. }
  1607. // check if is an array
  1608. function isArray(input) {
  1609. return Object.prototype.toString.call(input) === '[object Array]';
  1610. }
  1611. function isDate(input) {
  1612. return Object.prototype.toString.call(input) === '[object Date]' ||
  1613. input instanceof Date;
  1614. }
  1615. // compare two arrays, return the number of differences
  1616. function compareArrays(array1, array2, dontConvert) {
  1617. var len = Math.min(array1.length, array2.length),
  1618. lengthDiff = Math.abs(array1.length - array2.length),
  1619. diffs = 0,
  1620. i;
  1621. for (i = 0; i < len; i++) {
  1622. if ((dontConvert && array1[i] !== array2[i]) ||
  1623. (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) {
  1624. diffs++;
  1625. }
  1626. }
  1627. return diffs + lengthDiff;
  1628. }
  1629. function normalizeUnits(units) {
  1630. if (units) {
  1631. var lowered = units.toLowerCase().replace(/(.)s$/, '$1');
  1632. units = unitAliases[units] || camelFunctions[lowered] || lowered;
  1633. }
  1634. return units;
  1635. }
  1636. function normalizeObjectUnits(inputObject) {
  1637. var normalizedInput = {},
  1638. normalizedProp,
  1639. prop;
  1640. for (prop in inputObject) {
  1641. if (inputObject.hasOwnProperty(prop)) {
  1642. normalizedProp = normalizeUnits(prop);
  1643. if (normalizedProp) {
  1644. normalizedInput[normalizedProp] = inputObject[prop];
  1645. }
  1646. }
  1647. }
  1648. return normalizedInput;
  1649. }
  1650. function makeList(field) {
  1651. var count, setter;
  1652. if (field.indexOf('week') === 0) {
  1653. count = 7;
  1654. setter = 'day';
  1655. }
  1656. else if (field.indexOf('month') === 0) {
  1657. count = 12;
  1658. setter = 'month';
  1659. }
  1660. else {
  1661. return;
  1662. }
  1663. moment[field] = function (format, index) {
  1664. var i, getter,
  1665. method = moment.fn._lang[field],
  1666. results = [];
  1667. if (typeof format === 'number') {
  1668. index = format;
  1669. format = undefined;
  1670. }
  1671. getter = function (i) {
  1672. var m = moment().utc().set(setter, i);
  1673. return method.call(moment.fn._lang, m, format || '');
  1674. };
  1675. if (index != null) {
  1676. return getter(index);
  1677. }
  1678. else {
  1679. for (i = 0; i < count; i++) {
  1680. results.push(getter(i));
  1681. }
  1682. return results;
  1683. }
  1684. };
  1685. }
  1686. function toInt(argumentForCoercion) {
  1687. var coercedNumber = +argumentForCoercion,
  1688. value = 0;
  1689. if (coercedNumber !== 0 && isFinite(coercedNumber)) {
  1690. if (coercedNumber >= 0) {
  1691. value = Math.floor(coercedNumber);
  1692. } else {
  1693. value = Math.ceil(coercedNumber);
  1694. }
  1695. }
  1696. return value;
  1697. }
  1698. function daysInMonth(year, month) {
  1699. return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
  1700. }
  1701. function daysInYear(year) {
  1702. return isLeapYear(year) ? 366 : 365;
  1703. }
  1704. function isLeapYear(year) {
  1705. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  1706. }
  1707. function checkOverflow(m) {
  1708. var overflow;
  1709. if (m._a && m._pf.overflow === -2) {
  1710. overflow =
  1711. m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH :
  1712. m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE :
  1713. m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR :
  1714. m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE :
  1715. m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND :
  1716. m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND :
  1717. -1;
  1718. if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) {
  1719. overflow = DATE;
  1720. }
  1721. m._pf.overflow = overflow;
  1722. }
  1723. }
  1724. function initializeParsingFlags(config) {
  1725. config._pf = {
  1726. empty : false,
  1727. unusedTokens : [],
  1728. unusedInput : [],
  1729. overflow : -2,
  1730. charsLeftOver : 0,
  1731. nullInput : false,
  1732. invalidMonth : null,
  1733. invalidFormat : false,
  1734. userInvalidated : false,
  1735. iso: false
  1736. };
  1737. }
  1738. function isValid(m) {
  1739. if (m._isValid == null) {
  1740. m._isValid = !isNaN(m._d.getTime()) &&
  1741. m._pf.overflow < 0 &&
  1742. !m._pf.empty &&
  1743. !m._pf.invalidMonth &&
  1744. !m._pf.nullInput &&
  1745. !m._pf.invalidFormat &&
  1746. !m._pf.userInvalidated;
  1747. if (m._strict) {
  1748. m._isValid = m._isValid &&
  1749. m._pf.charsLeftOver === 0 &&
  1750. m._pf.unusedTokens.length === 0;
  1751. }
  1752. }
  1753. return m._isValid;
  1754. }
  1755. function normalizeLanguage(key) {
  1756. return key ? key.toLowerCase().replace('_', '-') : key;
  1757. }
  1758. // Return a moment from input, that is local/utc/zone equivalent to model.
  1759. function makeAs(input, model) {
  1760. return model._isUTC ? moment(input).zone(model._offset || 0) :
  1761. moment(input).local();
  1762. }
  1763. /************************************
  1764. Languages
  1765. ************************************/
  1766. extend(Language.prototype, {
  1767. set : function (config) {
  1768. var prop, i;
  1769. for (i in config) {
  1770. prop = config[i];
  1771. if (typeof prop === 'function') {
  1772. this[i] = prop;
  1773. } else {
  1774. this['_' + i] = prop;
  1775. }
  1776. }
  1777. },
  1778. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  1779. months : function (m) {
  1780. return this._months[m.month()];
  1781. },
  1782. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  1783. monthsShort : function (m) {
  1784. return this._monthsShort[m.month()];
  1785. },
  1786. monthsParse : function (monthName) {
  1787. var i, mom, regex;
  1788. if (!this._monthsParse) {
  1789. this._monthsParse = [];
  1790. }
  1791. for (i = 0; i < 12; i++) {
  1792. // make the regex if we don't have it already
  1793. if (!this._monthsParse[i]) {
  1794. mom = moment.utc([2000, i]);
  1795. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  1796. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  1797. }
  1798. // test the regex
  1799. if (this._monthsParse[i].test(monthName)) {
  1800. return i;
  1801. }
  1802. }
  1803. },
  1804. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  1805. weekdays : function (m) {
  1806. return this._weekdays[m.day()];
  1807. },
  1808. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  1809. weekdaysShort : function (m) {
  1810. return this._weekdaysShort[m.day()];
  1811. },
  1812. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  1813. weekdaysMin : function (m) {
  1814. return this._weekdaysMin[m.day()];
  1815. },
  1816. weekdaysParse : function (weekdayName) {
  1817. var i, mom, regex;
  1818. if (!this._weekdaysParse) {
  1819. this._weekdaysParse = [];
  1820. }
  1821. for (i = 0; i < 7; i++) {
  1822. // make the regex if we don't have it already
  1823. if (!this._weekdaysParse[i]) {
  1824. mom = moment([2000, 1]).day(i);
  1825. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  1826. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  1827. }
  1828. // test the regex
  1829. if (this._weekdaysParse[i].test(weekdayName)) {
  1830. return i;
  1831. }
  1832. }
  1833. },
  1834. _longDateFormat : {
  1835. LT : "h:mm A",
  1836. L : "MM/DD/YYYY",
  1837. LL : "MMMM D YYYY",
  1838. LLL : "MMMM D YYYY LT",
  1839. LLLL : "dddd, MMMM D YYYY LT"
  1840. },
  1841. longDateFormat : function (key) {
  1842. var output = this._longDateFormat[key];
  1843. if (!output && this._longDateFormat[key.toUpperCase()]) {
  1844. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  1845. return val.slice(1);
  1846. });
  1847. this._longDateFormat[key] = output;
  1848. }
  1849. return output;
  1850. },
  1851. isPM : function (input) {
  1852. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  1853. // Using charAt should be more compatible.
  1854. return ((input + '').toLowerCase().charAt(0) === 'p');
  1855. },
  1856. _meridiemParse : /[ap]\.?m?\.?/i,
  1857. meridiem : function (hours, minutes, isLower) {
  1858. if (hours > 11) {
  1859. return isLower ? 'pm' : 'PM';
  1860. } else {
  1861. return isLower ? 'am' : 'AM';
  1862. }
  1863. },
  1864. _calendar : {
  1865. sameDay : '[Today at] LT',
  1866. nextDay : '[Tomorrow at] LT',
  1867. nextWeek : 'dddd [at] LT',
  1868. lastDay : '[Yesterday at] LT',
  1869. lastWeek : '[Last] dddd [at] LT',
  1870. sameElse : 'L'
  1871. },
  1872. calendar : function (key, mom) {
  1873. var output = this._calendar[key];
  1874. return typeof output === 'function' ? output.apply(mom) : output;
  1875. },
  1876. _relativeTime : {
  1877. future : "in %s",
  1878. past : "%s ago",
  1879. s : "a few seconds",
  1880. m : "a minute",
  1881. mm : "%d minutes",
  1882. h : "an hour",
  1883. hh : "%d hours",
  1884. d : "a day",
  1885. dd : "%d days",
  1886. M : "a month",
  1887. MM : "%d months",
  1888. y : "a year",
  1889. yy : "%d years"
  1890. },
  1891. relativeTime : function (number, withoutSuffix, string, isFuture) {
  1892. var output = this._relativeTime[string];
  1893. return (typeof output === 'function') ?
  1894. output(number, withoutSuffix, string, isFuture) :
  1895. output.replace(/%d/i, number);
  1896. },
  1897. pastFuture : function (diff, output) {
  1898. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  1899. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  1900. },
  1901. ordinal : function (number) {
  1902. return this._ordinal.replace("%d", number);
  1903. },
  1904. _ordinal : "%d",
  1905. preparse : function (string) {
  1906. return string;
  1907. },
  1908. postformat : function (string) {
  1909. return string;
  1910. },
  1911. week : function (mom) {
  1912. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  1913. },
  1914. _week : {
  1915. dow : 0, // Sunday is the first day of the week.
  1916. doy : 6 // The week that contains Jan 1st is the first week of the year.
  1917. },
  1918. _invalidDate: 'Invalid date',
  1919. invalidDate: function () {
  1920. return this._invalidDate;
  1921. }
  1922. });
  1923. // Loads a language definition into the `languages` cache. The function
  1924. // takes a key and optionally values. If not in the browser and no values
  1925. // are provided, it will load the language file module. As a convenience,
  1926. // this function also returns the language values.
  1927. function loadLang(key, values) {
  1928. values.abbr = key;
  1929. if (!languages[key]) {
  1930. languages[key] = new Language();
  1931. }
  1932. languages[key].set(values);
  1933. return languages[key];
  1934. }
  1935. // Remove a language from the `languages` cache. Mostly useful in tests.
  1936. function unloadLang(key) {
  1937. delete languages[key];
  1938. }
  1939. // Determines which language definition to use and returns it.
  1940. //
  1941. // With no parameters, it will return the global language. If you
  1942. // pass in a language key, such as 'en', it will return the
  1943. // definition for 'en', so long as 'en' has already been loaded using
  1944. // moment.lang.
  1945. function getLangDefinition(key) {
  1946. var i = 0, j, lang, next, split,
  1947. get = function (k) {
  1948. if (!languages[k] && hasModule) {
  1949. try {
  1950. require('./lang/' + k);
  1951. } catch (e) { }
  1952. }
  1953. return languages[k];
  1954. };
  1955. if (!key) {
  1956. return moment.fn._lang;
  1957. }
  1958. if (!isArray(key)) {
  1959. //short-circuit everything else
  1960. lang = get(key);
  1961. if (lang) {
  1962. return lang;
  1963. }
  1964. key = [key];
  1965. }
  1966. //pick the language from the array
  1967. //try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each
  1968. //substring from most specific to least, but move to the next array item if it's a more specific variant than the current root
  1969. while (i < key.length) {
  1970. split = normalizeLanguage(key[i]).split('-');
  1971. j = split.length;
  1972. next = normalizeLanguage(key[i + 1]);
  1973. next = next ? next.split('-') : null;
  1974. while (j > 0) {
  1975. lang = get(split.slice(0, j).join('-'));
  1976. if (lang) {
  1977. return lang;
  1978. }
  1979. if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) {
  1980. //the next array item is better than a shallower substring of this one
  1981. break;
  1982. }
  1983. j--;
  1984. }
  1985. i++;
  1986. }
  1987. return moment.fn._lang;
  1988. }
  1989. /************************************
  1990. Formatting
  1991. ************************************/
  1992. function removeFormattingTokens(input) {
  1993. if (input.match(/\[[\s\S]/)) {
  1994. return input.replace(/^\[|\]$/g, "");
  1995. }
  1996. return input.replace(/\\/g, "");
  1997. }
  1998. function makeFormatFunction(format) {
  1999. var array = format.match(formattingTokens), i, length;
  2000. for (i = 0, length = array.length; i < length; i++) {
  2001. if (formatTokenFunctions[array[i]]) {
  2002. array[i] = formatTokenFunctions[array[i]];
  2003. } else {
  2004. array[i] = removeFormattingTokens(array[i]);
  2005. }
  2006. }
  2007. return function (mom) {
  2008. var output = "";
  2009. for (i = 0; i < length; i++) {
  2010. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  2011. }
  2012. return output;
  2013. };
  2014. }
  2015. // format date using native date object
  2016. function formatMoment(m, format) {
  2017. if (!m.isValid()) {
  2018. return m.lang().invalidDate();
  2019. }
  2020. format = expandFormat(format, m.lang());
  2021. if (!formatFunctions[format]) {
  2022. formatFunctions[format] = makeFormatFunction(format);
  2023. }
  2024. return formatFunctions[format](m);
  2025. }
  2026. function expandFormat(format, lang) {
  2027. var i = 5;
  2028. function replaceLongDateFormatTokens(input) {
  2029. return lang.longDateFormat(input) || input;
  2030. }
  2031. localFormattingTokens.lastIndex = 0;
  2032. while (i >= 0 && localFormattingTokens.test(format)) {
  2033. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  2034. localFormattingTokens.lastIndex = 0;
  2035. i -= 1;
  2036. }
  2037. return format;
  2038. }
  2039. /************************************
  2040. Parsing
  2041. ************************************/
  2042. // get the regex to find the next token
  2043. function getParseRegexForToken(token, config) {
  2044. var a, strict = config._strict;
  2045. switch (token) {
  2046. case 'DDDD':
  2047. return parseTokenThreeDigits;
  2048. case 'YYYY':
  2049. case 'GGGG':
  2050. case 'gggg':
  2051. return strict ? parseTokenFourDigits : parseTokenOneToFourDigits;
  2052. case 'YYYYYY':
  2053. case 'YYYYY':
  2054. case 'GGGGG':
  2055. case 'ggggg':
  2056. return strict ? parseTokenSixDigits : parseTokenOneToSixDigits;
  2057. case 'S':
  2058. if (strict) { return parseTokenOneDigit; }
  2059. /* falls through */
  2060. case 'SS':
  2061. if (strict) { return parseTokenTwoDigits; }
  2062. /* falls through */
  2063. case 'SSS':
  2064. case 'DDD':
  2065. return strict ? parseTokenThreeDigits : parseTokenOneToThreeDigits;
  2066. case 'MMM':
  2067. case 'MMMM':
  2068. case 'dd':
  2069. case 'ddd':
  2070. case 'dddd':
  2071. return parseTokenWord;
  2072. case 'a':
  2073. case 'A':
  2074. return getLangDefinition(config._l)._meridiemParse;
  2075. case 'X':
  2076. return parseTokenTimestampMs;
  2077. case 'Z':
  2078. case 'ZZ':
  2079. return parseTokenTimezone;
  2080. case 'T':
  2081. return parseTokenT;
  2082. case 'SSSS':
  2083. return parseTokenDigits;
  2084. case 'MM':
  2085. case 'DD':
  2086. case 'YY':
  2087. case 'GG':
  2088. case 'gg':
  2089. case 'HH':
  2090. case 'hh':
  2091. case 'mm':
  2092. case 'ss':
  2093. case 'ww':
  2094. case 'WW':
  2095. return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits;
  2096. case 'M':
  2097. case 'D':
  2098. case 'd':
  2099. case 'H':
  2100. case 'h':
  2101. case 'm':
  2102. case 's':
  2103. case 'w':
  2104. case 'W':
  2105. case 'e':
  2106. case 'E':
  2107. return strict ? parseTokenOneDigit : parseTokenOneOrTwoDigits;
  2108. default :
  2109. a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), "i"));
  2110. return a;
  2111. }
  2112. }
  2113. function timezoneMinutesFromString(string) {
  2114. string = string || "";
  2115. var possibleTzMatches = (string.match(parseTokenTimezone) || []),
  2116. tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [],
  2117. parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  2118. minutes = +(parts[1] * 60) + toInt(parts[2]);
  2119. return parts[0] === '+' ? -minutes : minutes;
  2120. }
  2121. // function to convert string input to date
  2122. function addTimeToArrayFromToken(token, input, config) {
  2123. var a, datePartArray = config._a;
  2124. switch (token) {
  2125. // MONTH
  2126. case 'M' : // fall through to MM
  2127. case 'MM' :
  2128. if (input != null) {
  2129. datePartArray[MONTH] = toInt(input) - 1;
  2130. }
  2131. break;
  2132. case 'MMM' : // fall through to MMMM
  2133. case 'MMMM' :
  2134. a = getLangDefinition(config._l).monthsParse(input);
  2135. // if we didn't find a month name, mark the date as invalid.
  2136. if (a != null) {
  2137. datePartArray[MONTH] = a;
  2138. } else {
  2139. config._pf.invalidMonth = input;
  2140. }
  2141. break;
  2142. // DAY OF MONTH
  2143. case 'D' : // fall through to DD
  2144. case 'DD' :
  2145. if (input != null) {
  2146. datePartArray[DATE] = toInt(input);
  2147. }
  2148. break;
  2149. // DAY OF YEAR
  2150. case 'DDD' : // fall through to DDDD
  2151. case 'DDDD' :
  2152. if (input != null) {
  2153. config._dayOfYear = toInt(input);
  2154. }
  2155. break;
  2156. // YEAR
  2157. case 'YY' :
  2158. datePartArray[YEAR] = toInt(input) + (toInt(input) > 68 ? 1900 : 2000);
  2159. break;
  2160. case 'YYYY' :
  2161. case 'YYYYY' :
  2162. case 'YYYYYY' :
  2163. datePartArray[YEAR] = toInt(input);
  2164. break;
  2165. // AM / PM
  2166. case 'a' : // fall through to A
  2167. case 'A' :
  2168. config._isPm = getLangDefinition(config._l).isPM(input);
  2169. break;
  2170. // 24 HOUR
  2171. case 'H' : // fall through to hh
  2172. case 'HH' : // fall through to hh
  2173. case 'h' : // fall through to hh
  2174. case 'hh' :
  2175. datePartArray[HOUR] = toInt(input);
  2176. break;
  2177. // MINUTE
  2178. case 'm' : // fall through to mm
  2179. case 'mm' :
  2180. datePartArray[MINUTE] = toInt(input);
  2181. break;
  2182. // SECOND
  2183. case 's' : // fall through to ss
  2184. case 'ss' :
  2185. datePartArray[SECOND] = toInt(input);
  2186. break;
  2187. // MILLISECOND
  2188. case 'S' :
  2189. case 'SS' :
  2190. case 'SSS' :
  2191. case 'SSSS' :
  2192. datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000);
  2193. break;
  2194. // UNIX TIMESTAMP WITH MS
  2195. case 'X':
  2196. config._d = new Date(parseFloat(input) * 1000);
  2197. break;
  2198. // TIMEZONE
  2199. case 'Z' : // fall through to ZZ
  2200. case 'ZZ' :
  2201. config._useUTC = true;
  2202. config._tzm = timezoneMinutesFromString(input);
  2203. break;
  2204. case 'w':
  2205. case 'ww':
  2206. case 'W':
  2207. case 'WW':
  2208. case 'd':
  2209. case 'dd':
  2210. case 'ddd':
  2211. case 'dddd':
  2212. case 'e':
  2213. case 'E':
  2214. token = token.substr(0, 1);
  2215. /* falls through */
  2216. case 'gg':
  2217. case 'gggg':
  2218. case 'GG':
  2219. case 'GGGG':
  2220. case 'GGGGG':
  2221. token = token.substr(0, 2);
  2222. if (input) {
  2223. config._w = config._w || {};
  2224. config._w[token] = input;
  2225. }
  2226. break;
  2227. }
  2228. }
  2229. // convert an array to a date.
  2230. // the array should mirror the parameters below
  2231. // note: all values past the year are optional and will default to the lowest possible value.
  2232. // [year, month, day , hour, minute, second, millisecond]
  2233. function dateFromConfig(config) {
  2234. var i, date, input = [], currentDate,
  2235. yearToUse, fixYear, w, temp, lang, weekday, week;
  2236. if (config._d) {
  2237. return;
  2238. }
  2239. currentDate = currentDateArray(config);
  2240. //compute day of the year from weeks and weekdays
  2241. if (config._w && config._a[DATE] == null && config._a[MONTH] == null) {
  2242. fixYear = function (val) {
  2243. var int_val = parseInt(val, 10);
  2244. return val ?
  2245. (val.length < 3 ? (int_val > 68 ? 1900 + int_val : 2000 + int_val) : int_val) :
  2246. (config._a[YEAR] == null ? moment().weekYear() : config._a[YEAR]);
  2247. };
  2248. w = config._w;
  2249. if (w.GG != null || w.W != null || w.E != null) {
  2250. temp = dayOfYearFromWeeks(fixYear(w.GG), w.W || 1, w.E, 4, 1);
  2251. }
  2252. else {
  2253. lang = getLangDefinition(config._l);
  2254. weekday = w.d != null ? parseWeekday(w.d, lang) :
  2255. (w.e != null ? parseInt(w.e, 10) + lang._week.dow : 0);
  2256. week = parseInt(w.w, 10) || 1;
  2257. //if we're parsing 'd', then the low day numbers may be next week
  2258. if (w.d != null && weekday < lang._week.dow) {
  2259. week++;
  2260. }
  2261. temp = dayOfYearFromWeeks(fixYear(w.gg), week, weekday, lang._week.doy, lang._week.dow);
  2262. }
  2263. config._a[YEAR] = temp.year;
  2264. config._dayOfYear = temp.dayOfYear;
  2265. }
  2266. //if the day of the year is set, figure out what it is
  2267. if (config._dayOfYear) {
  2268. yearToUse = config._a[YEAR] == null ? currentDate[YEAR] : config._a[YEAR];
  2269. if (config._dayOfYear > daysInYear(yearToUse)) {
  2270. config._pf._overflowDayOfYear = true;
  2271. }
  2272. date = makeUTCDate(yearToUse, 0, config._dayOfYear);
  2273. config._a[MONTH] = date.getUTCMonth();
  2274. config._a[DATE] = date.getUTCDate();
  2275. }
  2276. // Default to current date.
  2277. // * if no year, month, day of month are given, default to today
  2278. // * if day of month is given, default month and year
  2279. // * if month is given, default only year
  2280. // * if year is given, don't default anything
  2281. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  2282. config._a[i] = input[i] = currentDate[i];
  2283. }
  2284. // Zero out whatever was not defaulted, including time
  2285. for (; i < 7; i++) {
  2286. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  2287. }
  2288. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  2289. input[HOUR] += toInt((config._tzm || 0) / 60);
  2290. input[MINUTE] += toInt((config._tzm || 0) % 60);
  2291. config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input);
  2292. }
  2293. function dateFromObject(config) {
  2294. var normalizedInput;
  2295. if (config._d) {
  2296. return;
  2297. }
  2298. normalizedInput = normalizeObjectUnits(config._i);
  2299. config._a = [
  2300. normalizedInput.year,
  2301. normalizedInput.month,
  2302. normalizedInput.day,
  2303. normalizedInput.hour,
  2304. normalizedInput.minute,
  2305. normalizedInput.second,
  2306. normalizedInput.millisecond
  2307. ];
  2308. dateFromConfig(config);
  2309. }
  2310. function currentDateArray(config) {
  2311. var now = new Date();
  2312. if (config._useUTC) {
  2313. return [
  2314. now.getUTCFullYear(),
  2315. now.getUTCMonth(),
  2316. now.getUTCDate()
  2317. ];
  2318. } else {
  2319. return [now.getFullYear(), now.getMonth(), now.getDate()];
  2320. }
  2321. }
  2322. // date from string and format string
  2323. function makeDateFromStringAndFormat(config) {
  2324. config._a = [];
  2325. config._pf.empty = true;
  2326. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  2327. var lang = getLangDefinition(config._l),
  2328. string = '' + config._i,
  2329. i, parsedInput, tokens, token, skipped,
  2330. stringLength = string.length,
  2331. totalParsedInputLength = 0;
  2332. tokens = expandFormat(config._f, lang).match(formattingTokens) || [];
  2333. for (i = 0; i < tokens.length; i++) {
  2334. token = tokens[i];
  2335. parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0];
  2336. if (parsedInput) {
  2337. skipped = string.substr(0, string.indexOf(parsedInput));
  2338. if (skipped.length > 0) {
  2339. config._pf.unusedInput.push(skipped);
  2340. }
  2341. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  2342. totalParsedInputLength += parsedInput.length;
  2343. }
  2344. // don't parse if it's not a known token
  2345. if (formatTokenFunctions[token]) {
  2346. if (parsedInput) {
  2347. config._pf.empty = false;
  2348. }
  2349. else {
  2350. config._pf.unusedTokens.push(token);
  2351. }
  2352. addTimeToArrayFromToken(token, parsedInput, config);
  2353. }
  2354. else if (config._strict && !parsedInput) {
  2355. config._pf.unusedTokens.push(token);
  2356. }
  2357. }
  2358. // add remaining unparsed input length to the string
  2359. config._pf.charsLeftOver = stringLength - totalParsedInputLength;
  2360. if (string.length > 0) {
  2361. config._pf.unusedInput.push(string);
  2362. }
  2363. // handle am pm
  2364. if (config._isPm && config._a[HOUR] < 12) {
  2365. config._a[HOUR] += 12;
  2366. }
  2367. // if is 12 am, change hours to 0
  2368. if (config._isPm === false && config._a[HOUR] === 12) {
  2369. config._a[HOUR] = 0;
  2370. }
  2371. dateFromConfig(config);
  2372. checkOverflow(config);
  2373. }
  2374. function unescapeFormat(s) {
  2375. return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) {
  2376. return p1 || p2 || p3 || p4;
  2377. });
  2378. }
  2379. // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
  2380. function regexpEscape(s) {
  2381. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  2382. }
  2383. // date from string and array of format strings
  2384. function makeDateFromStringAndArray(config) {
  2385. var tempConfig,
  2386. bestMoment,
  2387. scoreToBeat,
  2388. i,
  2389. currentScore;
  2390. if (config._f.length === 0) {
  2391. config._pf.invalidFormat = true;
  2392. config._d = new Date(NaN);
  2393. return;
  2394. }
  2395. for (i = 0; i < config._f.length; i++) {
  2396. currentScore = 0;
  2397. tempConfig = extend({}, config);
  2398. initializeParsingFlags(tempConfig);
  2399. tempConfig._f = config._f[i];
  2400. makeDateFromStringAndFormat(tempConfig);
  2401. if (!isValid(tempConfig)) {
  2402. continue;
  2403. }
  2404. // if there is any input that was not parsed add a penalty for that format
  2405. currentScore += tempConfig._pf.charsLeftOver;
  2406. //or tokens
  2407. currentScore += tempConfig._pf.unusedTokens.length * 10;
  2408. tempConfig._pf.score = currentScore;
  2409. if (scoreToBeat == null || currentScore < scoreToBeat) {
  2410. scoreToBeat = currentScore;
  2411. bestMoment = tempConfig;
  2412. }
  2413. }
  2414. extend(config, bestMoment || tempConfig);
  2415. }
  2416. // date from iso format
  2417. function makeDateFromString(config) {
  2418. var i,
  2419. string = config._i,
  2420. match = isoRegex.exec(string);
  2421. if (match) {
  2422. config._pf.iso = true;
  2423. for (i = 4; i > 0; i--) {
  2424. if (match[i]) {
  2425. // match[5] should be "T" or undefined
  2426. config._f = isoDates[i - 1] + (match[6] || " ");
  2427. break;
  2428. }
  2429. }
  2430. for (i = 0; i < 4; i++) {
  2431. if (isoTimes[i][1].exec(string)) {
  2432. config._f += isoTimes[i][0];
  2433. break;
  2434. }
  2435. }
  2436. if (string.match(parseTokenTimezone)) {
  2437. config._f += "Z";
  2438. }
  2439. makeDateFromStringAndFormat(config);
  2440. }
  2441. else {
  2442. config._d = new Date(string);
  2443. }
  2444. }
  2445. function makeDateFromInput(config) {
  2446. var input = config._i,
  2447. matched = aspNetJsonRegex.exec(input);
  2448. if (input === undefined) {
  2449. config._d = new Date();
  2450. } else if (matched) {
  2451. config._d = new Date(+matched[1]);
  2452. } else if (typeof input === 'string') {
  2453. makeDateFromString(config);
  2454. } else if (isArray(input)) {
  2455. config._a = input.slice(0);
  2456. dateFromConfig(config);
  2457. } else if (isDate(input)) {
  2458. config._d = new Date(+input);
  2459. } else if (typeof(input) === 'object') {
  2460. dateFromObject(config);
  2461. } else {
  2462. config._d = new Date(input);
  2463. }
  2464. }
  2465. function makeDate(y, m, d, h, M, s, ms) {
  2466. //can't just apply() to create a date:
  2467. //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply
  2468. var date = new Date(y, m, d, h, M, s, ms);
  2469. //the date constructor doesn't accept years < 1970
  2470. if (y < 1970) {
  2471. date.setFullYear(y);
  2472. }
  2473. return date;
  2474. }
  2475. function makeUTCDate(y) {
  2476. var date = new Date(Date.UTC.apply(null, arguments));
  2477. if (y < 1970) {
  2478. date.setUTCFullYear(y);
  2479. }
  2480. return date;
  2481. }
  2482. function parseWeekday(input, language) {
  2483. if (typeof input === 'string') {
  2484. if (!isNaN(input)) {
  2485. input = parseInt(input, 10);
  2486. }
  2487. else {
  2488. input = language.weekdaysParse(input);
  2489. if (typeof input !== 'number') {
  2490. return null;
  2491. }
  2492. }
  2493. }
  2494. return input;
  2495. }
  2496. /************************************
  2497. Relative Time
  2498. ************************************/
  2499. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  2500. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  2501. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  2502. }
  2503. function relativeTime(milliseconds, withoutSuffix, lang) {
  2504. var seconds = round(Math.abs(milliseconds) / 1000),
  2505. minutes = round(seconds / 60),
  2506. hours = round(minutes / 60),
  2507. days = round(hours / 24),
  2508. years = round(days / 365),
  2509. args = seconds < 45 && ['s', seconds] ||
  2510. minutes === 1 && ['m'] ||
  2511. minutes < 45 && ['mm', minutes] ||
  2512. hours === 1 && ['h'] ||
  2513. hours < 22 && ['hh', hours] ||
  2514. days === 1 && ['d'] ||
  2515. days <= 25 && ['dd', days] ||
  2516. days <= 45 && ['M'] ||
  2517. days < 345 && ['MM', round(days / 30)] ||
  2518. years === 1 && ['y'] || ['yy', years];
  2519. args[2] = withoutSuffix;
  2520. args[3] = milliseconds > 0;
  2521. args[4] = lang;
  2522. return substituteTimeAgo.apply({}, args);
  2523. }
  2524. /************************************
  2525. Week of Year
  2526. ************************************/
  2527. // firstDayOfWeek 0 = sun, 6 = sat
  2528. // the day of the week that starts the week
  2529. // (usually sunday or monday)
  2530. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  2531. // the first week is the week that contains the first
  2532. // of this day of the week
  2533. // (eg. ISO weeks use thursday (4))
  2534. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  2535. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  2536. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  2537. adjustedMoment;
  2538. if (daysToDayOfWeek > end) {
  2539. daysToDayOfWeek -= 7;
  2540. }
  2541. if (daysToDayOfWeek < end - 7) {
  2542. daysToDayOfWeek += 7;
  2543. }
  2544. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  2545. return {
  2546. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  2547. year: adjustedMoment.year()
  2548. };
  2549. }
  2550. //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday
  2551. function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) {
  2552. // The only solid way to create an iso date from year is to use
  2553. // a string format (Date.UTC handles only years > 1900). Don't ask why
  2554. // it doesn't need Z at the end.
  2555. var d = new Date(leftZeroFill(year, 6, true) + '-01-01').getUTCDay(),
  2556. daysToAdd, dayOfYear;
  2557. weekday = weekday != null ? weekday : firstDayOfWeek;
  2558. daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0);
  2559. dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1;
  2560. return {
  2561. year: dayOfYear > 0 ? year : year - 1,
  2562. dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear
  2563. };
  2564. }
  2565. /************************************
  2566. Top Level Functions
  2567. ************************************/
  2568. function makeMoment(config) {
  2569. var input = config._i,
  2570. format = config._f;
  2571. if (typeof config._pf === 'undefined') {
  2572. initializeParsingFlags(config);
  2573. }
  2574. if (input === null) {
  2575. return moment.invalid({nullInput: true});
  2576. }
  2577. if (typeof input === 'string') {
  2578. config._i = input = getLangDefinition().preparse(input);
  2579. }
  2580. if (moment.isMoment(input)) {
  2581. config = extend({}, input);
  2582. config._d = new Date(+input._d);
  2583. } else if (format) {
  2584. if (isArray(format)) {
  2585. makeDateFromStringAndArray(config);
  2586. } else {
  2587. makeDateFromStringAndFormat(config);
  2588. }
  2589. } else {
  2590. makeDateFromInput(config);
  2591. }
  2592. return new Moment(config);
  2593. }
  2594. moment = function (input, format, lang, strict) {
  2595. if (typeof(lang) === "boolean") {
  2596. strict = lang;
  2597. lang = undefined;
  2598. }
  2599. return makeMoment({
  2600. _i : input,
  2601. _f : format,
  2602. _l : lang,
  2603. _strict : strict,
  2604. _isUTC : false
  2605. });
  2606. };
  2607. // creating with utc
  2608. moment.utc = function (input, format, lang, strict) {
  2609. var m;
  2610. if (typeof(lang) === "boolean") {
  2611. strict = lang;
  2612. lang = undefined;
  2613. }
  2614. m = makeMoment({
  2615. _useUTC : true,
  2616. _isUTC : true,
  2617. _l : lang,
  2618. _i : input,
  2619. _f : format,
  2620. _strict : strict
  2621. }).utc();
  2622. return m;
  2623. };
  2624. // creating with unix timestamp (in seconds)
  2625. moment.unix = function (input) {
  2626. return moment(input * 1000);
  2627. };
  2628. // duration
  2629. moment.duration = function (input, key) {
  2630. var duration = input,
  2631. // matching against regexp is expensive, do it on demand
  2632. match = null,
  2633. sign,
  2634. ret,
  2635. parseIso;
  2636. if (moment.isDuration(input)) {
  2637. duration = {
  2638. ms: input._milliseconds,
  2639. d: input._days,
  2640. M: input._months
  2641. };
  2642. } else if (typeof input === 'number') {
  2643. duration = {};
  2644. if (key) {
  2645. duration[key] = input;
  2646. } else {
  2647. duration.milliseconds = input;
  2648. }
  2649. } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) {
  2650. sign = (match[1] === "-") ? -1 : 1;
  2651. duration = {
  2652. y: 0,
  2653. d: toInt(match[DATE]) * sign,
  2654. h: toInt(match[HOUR]) * sign,
  2655. m: toInt(match[MINUTE]) * sign,
  2656. s: toInt(match[SECOND]) * sign,
  2657. ms: toInt(match[MILLISECOND]) * sign
  2658. };
  2659. } else if (!!(match = isoDurationRegex.exec(input))) {
  2660. sign = (match[1] === "-") ? -1 : 1;
  2661. parseIso = function (inp) {
  2662. // We'd normally use ~~inp for this, but unfortunately it also
  2663. // converts floats to ints.
  2664. // inp may be undefined, so careful calling replace on it.
  2665. var res = inp && parseFloat(inp.replace(',', '.'));
  2666. // apply sign while we're at it
  2667. return (isNaN(res) ? 0 : res) * sign;
  2668. };
  2669. duration = {
  2670. y: parseIso(match[2]),
  2671. M: parseIso(match[3]),
  2672. d: parseIso(match[4]),
  2673. h: parseIso(match[5]),
  2674. m: parseIso(match[6]),
  2675. s: parseIso(match[7]),
  2676. w: parseIso(match[8])
  2677. };
  2678. }
  2679. ret = new Duration(duration);
  2680. if (moment.isDuration(input) && input.hasOwnProperty('_lang')) {
  2681. ret._lang = input._lang;
  2682. }
  2683. return ret;
  2684. };
  2685. // version number
  2686. moment.version = VERSION;
  2687. // default format
  2688. moment.defaultFormat = isoFormat;
  2689. // This function will be called whenever a moment is mutated.
  2690. // It is intended to keep the offset in sync with the timezone.
  2691. moment.updateOffset = function () {};
  2692. // This function will load languages and then set the global language. If
  2693. // no arguments are passed in, it will simply return the current global
  2694. // language key.
  2695. moment.lang = function (key, values) {
  2696. var r;
  2697. if (!key) {
  2698. return moment.fn._lang._abbr;
  2699. }
  2700. if (values) {
  2701. loadLang(normalizeLanguage(key), values);
  2702. } else if (values === null) {
  2703. unloadLang(key);
  2704. key = 'en';
  2705. } else if (!languages[key]) {
  2706. getLangDefinition(key);
  2707. }
  2708. r = moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  2709. return r._abbr;
  2710. };
  2711. // returns language data
  2712. moment.langData = function (key) {
  2713. if (key && key._lang && key._lang._abbr) {
  2714. key = key._lang._abbr;
  2715. }
  2716. return getLangDefinition(key);
  2717. };
  2718. // compare moment object
  2719. moment.isMoment = function (obj) {
  2720. return obj instanceof Moment;
  2721. };
  2722. // for typechecking Duration objects
  2723. moment.isDuration = function (obj) {
  2724. return obj instanceof Duration;
  2725. };
  2726. for (i = lists.length - 1; i >= 0; --i) {
  2727. makeList(lists[i]);
  2728. }
  2729. moment.normalizeUnits = function (units) {
  2730. return normalizeUnits(units);
  2731. };
  2732. moment.invalid = function (flags) {
  2733. var m = moment.utc(NaN);
  2734. if (flags != null) {
  2735. extend(m._pf, flags);
  2736. }
  2737. else {
  2738. m._pf.userInvalidated = true;
  2739. }
  2740. return m;
  2741. };
  2742. moment.parseZone = function (input) {
  2743. return moment(input).parseZone();
  2744. };
  2745. /************************************
  2746. Moment Prototype
  2747. ************************************/
  2748. extend(moment.fn = Moment.prototype, {
  2749. clone : function () {
  2750. return moment(this);
  2751. },
  2752. valueOf : function () {
  2753. return +this._d + ((this._offset || 0) * 60000);
  2754. },
  2755. unix : function () {
  2756. return Math.floor(+this / 1000);
  2757. },
  2758. toString : function () {
  2759. return this.clone().lang('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  2760. },
  2761. toDate : function () {
  2762. return this._offset ? new Date(+this) : this._d;
  2763. },
  2764. toISOString : function () {
  2765. var m = moment(this).utc();
  2766. if (0 < m.year() && m.year() <= 9999) {
  2767. return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  2768. } else {
  2769. return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  2770. }
  2771. },
  2772. toArray : function () {
  2773. var m = this;
  2774. return [
  2775. m.year(),
  2776. m.month(),
  2777. m.date(),
  2778. m.hours(),
  2779. m.minutes(),
  2780. m.seconds(),
  2781. m.milliseconds()
  2782. ];
  2783. },
  2784. isValid : function () {
  2785. return isValid(this);
  2786. },
  2787. isDSTShifted : function () {
  2788. if (this._a) {
  2789. return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0;
  2790. }
  2791. return false;
  2792. },
  2793. parsingFlags : function () {
  2794. return extend({}, this._pf);
  2795. },
  2796. invalidAt: function () {
  2797. return this._pf.overflow;
  2798. },
  2799. utc : function () {
  2800. return this.zone(0);
  2801. },
  2802. local : function () {
  2803. this.zone(0);
  2804. this._isUTC = false;
  2805. return this;
  2806. },
  2807. format : function (inputString) {
  2808. var output = formatMoment(this, inputString || moment.defaultFormat);
  2809. return this.lang().postformat(output);
  2810. },
  2811. add : function (input, val) {
  2812. var dur;
  2813. // switch args to support add('s', 1) and add(1, 's')
  2814. if (typeof input === 'string') {
  2815. dur = moment.duration(+val, input);
  2816. } else {
  2817. dur = moment.duration(input, val);
  2818. }
  2819. addOrSubtractDurationFromMoment(this, dur, 1);
  2820. return this;
  2821. },
  2822. subtract : function (input, val) {
  2823. var dur;
  2824. // switch args to support subtract('s', 1) and subtract(1, 's')
  2825. if (typeof input === 'string') {
  2826. dur = moment.duration(+val, input);
  2827. } else {
  2828. dur = moment.duration(input, val);
  2829. }
  2830. addOrSubtractDurationFromMoment(this, dur, -1);
  2831. return this;
  2832. },
  2833. diff : function (input, units, asFloat) {
  2834. var that = makeAs(input, this),
  2835. zoneDiff = (this.zone() - that.zone()) * 6e4,
  2836. diff, output;
  2837. units = normalizeUnits(units);
  2838. if (units === 'year' || units === 'month') {
  2839. // average number of days in the months in the given dates
  2840. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  2841. // difference in months
  2842. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  2843. // adjust by taking difference in days, average number of days
  2844. // and dst in the given months.
  2845. output += ((this - moment(this).startOf('month')) -
  2846. (that - moment(that).startOf('month'))) / diff;
  2847. // same as above but with zones, to negate all dst
  2848. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  2849. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  2850. if (units === 'year') {
  2851. output = output / 12;
  2852. }
  2853. } else {
  2854. diff = (this - that);
  2855. output = units === 'second' ? diff / 1e3 : // 1000
  2856. units === 'minute' ? diff / 6e4 : // 1000 * 60
  2857. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  2858. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  2859. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  2860. diff;
  2861. }
  2862. return asFloat ? output : absRound(output);
  2863. },
  2864. from : function (time, withoutSuffix) {
  2865. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  2866. },
  2867. fromNow : function (withoutSuffix) {
  2868. return this.from(moment(), withoutSuffix);
  2869. },
  2870. calendar : function () {
  2871. // We want to compare the start of today, vs this.
  2872. // Getting start-of-today depends on whether we're zone'd or not.
  2873. var sod = makeAs(moment(), this).startOf('day'),
  2874. diff = this.diff(sod, 'days', true),
  2875. format = diff < -6 ? 'sameElse' :
  2876. diff < -1 ? 'lastWeek' :
  2877. diff < 0 ? 'lastDay' :
  2878. diff < 1 ? 'sameDay' :
  2879. diff < 2 ? 'nextDay' :
  2880. diff < 7 ? 'nextWeek' : 'sameElse';
  2881. return this.format(this.lang().calendar(format, this));
  2882. },
  2883. isLeapYear : function () {
  2884. return isLeapYear(this.year());
  2885. },
  2886. isDST : function () {
  2887. return (this.zone() < this.clone().month(0).zone() ||
  2888. this.zone() < this.clone().month(5).zone());
  2889. },
  2890. day : function (input) {
  2891. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  2892. if (input != null) {
  2893. input = parseWeekday(input, this.lang());
  2894. return this.add({ d : input - day });
  2895. } else {
  2896. return day;
  2897. }
  2898. },
  2899. month : function (input) {
  2900. var utc = this._isUTC ? 'UTC' : '',
  2901. dayOfMonth;
  2902. if (input != null) {
  2903. if (typeof input === 'string') {
  2904. input = this.lang().monthsParse(input);
  2905. if (typeof input !== 'number') {
  2906. return this;
  2907. }
  2908. }
  2909. dayOfMonth = this.date();
  2910. this.date(1);
  2911. this._d['set' + utc + 'Month'](input);
  2912. this.date(Math.min(dayOfMonth, this.daysInMonth()));
  2913. moment.updateOffset(this);
  2914. return this;
  2915. } else {
  2916. return this._d['get' + utc + 'Month']();
  2917. }
  2918. },
  2919. startOf: function (units) {
  2920. units = normalizeUnits(units);
  2921. // the following switch intentionally omits break keywords
  2922. // to utilize falling through the cases.
  2923. switch (units) {
  2924. case 'year':
  2925. this.month(0);
  2926. /* falls through */
  2927. case 'month':
  2928. this.date(1);
  2929. /* falls through */
  2930. case 'week':
  2931. case 'isoWeek':
  2932. case 'day':
  2933. this.hours(0);
  2934. /* falls through */
  2935. case 'hour':
  2936. this.minutes(0);
  2937. /* falls through */
  2938. case 'minute':
  2939. this.seconds(0);
  2940. /* falls through */
  2941. case 'second':
  2942. this.milliseconds(0);
  2943. /* falls through */
  2944. }
  2945. // weeks are a special case
  2946. if (units === 'week') {
  2947. this.weekday(0);
  2948. } else if (units === 'isoWeek') {
  2949. this.isoWeekday(1);
  2950. }
  2951. return this;
  2952. },
  2953. endOf: function (units) {
  2954. units = normalizeUnits(units);
  2955. return this.startOf(units).add((units === 'isoWeek' ? 'week' : units), 1).subtract('ms', 1);
  2956. },
  2957. isAfter: function (input, units) {
  2958. units = typeof units !== 'undefined' ? units : 'millisecond';
  2959. return +this.clone().startOf(units) > +moment(input).startOf(units);
  2960. },
  2961. isBefore: function (input, units) {
  2962. units = typeof units !== 'undefined' ? units : 'millisecond';
  2963. return +this.clone().startOf(units) < +moment(input).startOf(units);
  2964. },
  2965. isSame: function (input, units) {
  2966. units = units || 'ms';
  2967. return +this.clone().startOf(units) === +makeAs(input, this).startOf(units);
  2968. },
  2969. min: function (other) {
  2970. other = moment.apply(null, arguments);
  2971. return other < this ? this : other;
  2972. },
  2973. max: function (other) {
  2974. other = moment.apply(null, arguments);
  2975. return other > this ? this : other;
  2976. },
  2977. zone : function (input) {
  2978. var offset = this._offset || 0;
  2979. if (input != null) {
  2980. if (typeof input === "string") {
  2981. input = timezoneMinutesFromString(input);
  2982. }
  2983. if (Math.abs(input) < 16) {
  2984. input = input * 60;
  2985. }
  2986. this._offset = input;
  2987. this._isUTC = true;
  2988. if (offset !== input) {
  2989. addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
  2990. }
  2991. } else {
  2992. return this._isUTC ? offset : this._d.getTimezoneOffset();
  2993. }
  2994. return this;
  2995. },
  2996. zoneAbbr : function () {
  2997. return this._isUTC ? "UTC" : "";
  2998. },
  2999. zoneName : function () {
  3000. return this._isUTC ? "Coordinated Universal Time" : "";
  3001. },
  3002. parseZone : function () {
  3003. if (this._tzm) {
  3004. this.zone(this._tzm);
  3005. } else if (typeof this._i === 'string') {
  3006. this.zone(this._i);
  3007. }
  3008. return this;
  3009. },
  3010. hasAlignedHourOffset : function (input) {
  3011. if (!input) {
  3012. input = 0;
  3013. }
  3014. else {
  3015. input = moment(input).zone();
  3016. }
  3017. return (this.zone() - input) % 60 === 0;
  3018. },
  3019. daysInMonth : function () {
  3020. return daysInMonth(this.year(), this.month());
  3021. },
  3022. dayOfYear : function (input) {
  3023. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  3024. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  3025. },
  3026. quarter : function () {
  3027. return Math.ceil((this.month() + 1.0) / 3.0);
  3028. },
  3029. weekYear : function (input) {
  3030. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  3031. return input == null ? year : this.add("y", (input - year));
  3032. },
  3033. isoWeekYear : function (input) {
  3034. var year = weekOfYear(this, 1, 4).year;
  3035. return input == null ? year : this.add("y", (input - year));
  3036. },
  3037. week : function (input) {
  3038. var week = this.lang().week(this);
  3039. return input == null ? week : this.add("d", (input - week) * 7);
  3040. },
  3041. isoWeek : function (input) {
  3042. var week = weekOfYear(this, 1, 4).week;
  3043. return input == null ? week : this.add("d", (input - week) * 7);
  3044. },
  3045. weekday : function (input) {
  3046. var weekday = (this.day() + 7 - this.lang()._week.dow) % 7;
  3047. return input == null ? weekday : this.add("d", input - weekday);
  3048. },
  3049. isoWeekday : function (input) {
  3050. // behaves the same as moment#day except
  3051. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  3052. // as a setter, sunday should belong to the previous week.
  3053. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  3054. },
  3055. get : function (units) {
  3056. units = normalizeUnits(units);
  3057. return this[units]();
  3058. },
  3059. set : function (units, value) {
  3060. units = normalizeUnits(units);
  3061. if (typeof this[units] === 'function') {
  3062. this[units](value);
  3063. }
  3064. return this;
  3065. },
  3066. // If passed a language key, it will set the language for this
  3067. // instance. Otherwise, it will return the language configuration
  3068. // variables for this instance.
  3069. lang : function (key) {
  3070. if (key === undefined) {
  3071. return this._lang;
  3072. } else {
  3073. this._lang = getLangDefinition(key);
  3074. return this;
  3075. }
  3076. }
  3077. });
  3078. // helper for adding shortcuts
  3079. function makeGetterAndSetter(name, key) {
  3080. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  3081. var utc = this._isUTC ? 'UTC' : '';
  3082. if (input != null) {
  3083. this._d['set' + utc + key](input);
  3084. moment.updateOffset(this);
  3085. return this;
  3086. } else {
  3087. return this._d['get' + utc + key]();
  3088. }
  3089. };
  3090. }
  3091. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  3092. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  3093. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  3094. }
  3095. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  3096. makeGetterAndSetter('year', 'FullYear');
  3097. // add plural methods
  3098. moment.fn.days = moment.fn.day;
  3099. moment.fn.months = moment.fn.month;
  3100. moment.fn.weeks = moment.fn.week;
  3101. moment.fn.isoWeeks = moment.fn.isoWeek;
  3102. // add aliased format methods
  3103. moment.fn.toJSON = moment.fn.toISOString;
  3104. /************************************
  3105. Duration Prototype
  3106. ************************************/
  3107. extend(moment.duration.fn = Duration.prototype, {
  3108. _bubble : function () {
  3109. var milliseconds = this._milliseconds,
  3110. days = this._days,
  3111. months = this._months,
  3112. data = this._data,
  3113. seconds, minutes, hours, years;
  3114. // The following code bubbles up values, see the tests for
  3115. // examples of what that means.
  3116. data.milliseconds = milliseconds % 1000;
  3117. seconds = absRound(milliseconds / 1000);
  3118. data.seconds = seconds % 60;
  3119. minutes = absRound(seconds / 60);
  3120. data.minutes = minutes % 60;
  3121. hours = absRound(minutes / 60);
  3122. data.hours = hours % 24;
  3123. days += absRound(hours / 24);
  3124. data.days = days % 30;
  3125. months += absRound(days / 30);
  3126. data.months = months % 12;
  3127. years = absRound(months / 12);
  3128. data.years = years;
  3129. },
  3130. weeks : function () {
  3131. return absRound(this.days() / 7);
  3132. },
  3133. valueOf : function () {
  3134. return this._milliseconds +
  3135. this._days * 864e5 +
  3136. (this._months % 12) * 2592e6 +
  3137. toInt(this._months / 12) * 31536e6;
  3138. },
  3139. humanize : function (withSuffix) {
  3140. var difference = +this,
  3141. output = relativeTime(difference, !withSuffix, this.lang());
  3142. if (withSuffix) {
  3143. output = this.lang().pastFuture(difference, output);
  3144. }
  3145. return this.lang().postformat(output);
  3146. },
  3147. add : function (input, val) {
  3148. // supports only 2.0-style add(1, 's') or add(moment)
  3149. var dur = moment.duration(input, val);
  3150. this._milliseconds += dur._milliseconds;
  3151. this._days += dur._days;
  3152. this._months += dur._months;
  3153. this._bubble();
  3154. return this;
  3155. },
  3156. subtract : function (input, val) {
  3157. var dur = moment.duration(input, val);
  3158. this._milliseconds -= dur._milliseconds;
  3159. this._days -= dur._days;
  3160. this._months -= dur._months;
  3161. this._bubble();
  3162. return this;
  3163. },
  3164. get : function (units) {
  3165. units = normalizeUnits(units);
  3166. return this[units.toLowerCase() + 's']();
  3167. },
  3168. as : function (units) {
  3169. units = normalizeUnits(units);
  3170. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  3171. },
  3172. lang : moment.fn.lang,
  3173. toIsoString : function () {
  3174. // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js
  3175. var years = Math.abs(this.years()),
  3176. months = Math.abs(this.months()),
  3177. days = Math.abs(this.days()),
  3178. hours = Math.abs(this.hours()),
  3179. minutes = Math.abs(this.minutes()),
  3180. seconds = Math.abs(this.seconds() + this.milliseconds() / 1000);
  3181. if (!this.asSeconds()) {
  3182. // this is the same as C#'s (Noda) and python (isodate)...
  3183. // but not other JS (goog.date)
  3184. return 'P0D';
  3185. }
  3186. return (this.asSeconds() < 0 ? '-' : '') +
  3187. 'P' +
  3188. (years ? years + 'Y' : '') +
  3189. (months ? months + 'M' : '') +
  3190. (days ? days + 'D' : '') +
  3191. ((hours || minutes || seconds) ? 'T' : '') +
  3192. (hours ? hours + 'H' : '') +
  3193. (minutes ? minutes + 'M' : '') +
  3194. (seconds ? seconds + 'S' : '');
  3195. }
  3196. });
  3197. function makeDurationGetter(name) {
  3198. moment.duration.fn[name] = function () {
  3199. return this._data[name];
  3200. };
  3201. }
  3202. function makeDurationAsGetter(name, factor) {
  3203. moment.duration.fn['as' + name] = function () {
  3204. return +this / factor;
  3205. };
  3206. }
  3207. for (i in unitMillisecondFactors) {
  3208. if (unitMillisecondFactors.hasOwnProperty(i)) {
  3209. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  3210. makeDurationGetter(i.toLowerCase());
  3211. }
  3212. }
  3213. makeDurationAsGetter('Weeks', 6048e5);
  3214. moment.duration.fn.asMonths = function () {
  3215. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  3216. };
  3217. /************************************
  3218. Default Lang
  3219. ************************************/
  3220. // Set default language, other languages will inherit from English.
  3221. moment.lang('en', {
  3222. ordinal : function (number) {
  3223. var b = number % 10,
  3224. output = (toInt(number % 100 / 10) === 1) ? 'th' :
  3225. (b === 1) ? 'st' :
  3226. (b === 2) ? 'nd' :
  3227. (b === 3) ? 'rd' : 'th';
  3228. return number + output;
  3229. }
  3230. });
  3231. /* EMBED_LANGUAGES */
  3232. /************************************
  3233. Exposing Moment
  3234. ************************************/
  3235. function makeGlobal(deprecate) {
  3236. var warned = false, local_moment = moment;
  3237. /*global ender:false */
  3238. if (typeof ender !== 'undefined') {
  3239. return;
  3240. }
  3241. // here, `this` means `window` in the browser, or `global` on the server
  3242. // add `moment` as a global object via a string identifier,
  3243. // for Closure Compiler "advanced" mode
  3244. if (deprecate) {
  3245. global.moment = function () {
  3246. if (!warned && console && console.warn) {
  3247. warned = true;
  3248. console.warn(
  3249. "Accessing Moment through the global scope is " +
  3250. "deprecated, and will be removed in an upcoming " +
  3251. "release.");
  3252. }
  3253. return local_moment.apply(null, arguments);
  3254. };
  3255. extend(global.moment, local_moment);
  3256. } else {
  3257. global['moment'] = moment;
  3258. }
  3259. }
  3260. // CommonJS module is defined
  3261. if (hasModule) {
  3262. module.exports = moment;
  3263. makeGlobal(true);
  3264. } else if (typeof define === "function" && define.amd) {
  3265. define("moment", function (require, exports, module) {
  3266. if (module.config && module.config() && module.config().noGlobal !== true) {
  3267. // If user provided noGlobal, he is aware of global
  3268. makeGlobal(module.config().noGlobal === undefined);
  3269. }
  3270. return moment;
  3271. });
  3272. } else {
  3273. makeGlobal();
  3274. }
  3275. }).call(this);
  3276. },{}],3:[function(require,module,exports){
  3277. /**
  3278. * Copyright 2012 Craig Campbell
  3279. *
  3280. * Licensed under the Apache License, Version 2.0 (the "License");
  3281. * you may not use this file except in compliance with the License.
  3282. * You may obtain a copy of the License at
  3283. *
  3284. * http://www.apache.org/licenses/LICENSE-2.0
  3285. *
  3286. * Unless required by applicable law or agreed to in writing, software
  3287. * distributed under the License is distributed on an "AS IS" BASIS,
  3288. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  3289. * See the License for the specific language governing permissions and
  3290. * limitations under the License.
  3291. *
  3292. * Mousetrap is a simple keyboard shortcut library for Javascript with
  3293. * no external dependencies
  3294. *
  3295. * @version 1.1.2
  3296. * @url craig.is/killing/mice
  3297. */
  3298. /**
  3299. * mapping of special keycodes to their corresponding keys
  3300. *
  3301. * everything in this dictionary cannot use keypress events
  3302. * so it has to be here to map to the correct keycodes for
  3303. * keyup/keydown events
  3304. *
  3305. * @type {Object}
  3306. */
  3307. var _MAP = {
  3308. 8: 'backspace',
  3309. 9: 'tab',
  3310. 13: 'enter',
  3311. 16: 'shift',
  3312. 17: 'ctrl',
  3313. 18: 'alt',
  3314. 20: 'capslock',
  3315. 27: 'esc',
  3316. 32: 'space',
  3317. 33: 'pageup',
  3318. 34: 'pagedown',
  3319. 35: 'end',
  3320. 36: 'home',
  3321. 37: 'left',
  3322. 38: 'up',
  3323. 39: 'right',
  3324. 40: 'down',
  3325. 45: 'ins',
  3326. 46: 'del',
  3327. 91: 'meta',
  3328. 93: 'meta',
  3329. 224: 'meta'
  3330. },
  3331. /**
  3332. * mapping for special characters so they can support
  3333. *
  3334. * this dictionary is only used incase you want to bind a
  3335. * keyup or keydown event to one of these keys
  3336. *
  3337. * @type {Object}
  3338. */
  3339. _KEYCODE_MAP = {
  3340. 106: '*',
  3341. 107: '+',
  3342. 109: '-',
  3343. 110: '.',
  3344. 111 : '/',
  3345. 186: ';',
  3346. 187: '=',
  3347. 188: ',',
  3348. 189: '-',
  3349. 190: '.',
  3350. 191: '/',
  3351. 192: '`',
  3352. 219: '[',
  3353. 220: '\\',
  3354. 221: ']',
  3355. 222: '\''
  3356. },
  3357. /**
  3358. * this is a mapping of keys that require shift on a US keypad
  3359. * back to the non shift equivelents
  3360. *
  3361. * this is so you can use keyup events with these keys
  3362. *
  3363. * note that this will only work reliably on US keyboards
  3364. *
  3365. * @type {Object}
  3366. */
  3367. _SHIFT_MAP = {
  3368. '~': '`',
  3369. '!': '1',
  3370. '@': '2',
  3371. '#': '3',
  3372. '$': '4',
  3373. '%': '5',
  3374. '^': '6',
  3375. '&': '7',
  3376. '*': '8',
  3377. '(': '9',
  3378. ')': '0',
  3379. '_': '-',
  3380. '+': '=',
  3381. ':': ';',
  3382. '\"': '\'',
  3383. '<': ',',
  3384. '>': '.',
  3385. '?': '/',
  3386. '|': '\\'
  3387. },
  3388. /**
  3389. * this is a list of special strings you can use to map
  3390. * to modifier keys when you specify your keyboard shortcuts
  3391. *
  3392. * @type {Object}
  3393. */
  3394. _SPECIAL_ALIASES = {
  3395. 'option': 'alt',
  3396. 'command': 'meta',
  3397. 'return': 'enter',
  3398. 'escape': 'esc'
  3399. },
  3400. /**
  3401. * variable to store the flipped version of _MAP from above
  3402. * needed to check if we should use keypress or not when no action
  3403. * is specified
  3404. *
  3405. * @type {Object|undefined}
  3406. */
  3407. _REVERSE_MAP,
  3408. /**
  3409. * a list of all the callbacks setup via Mousetrap.bind()
  3410. *
  3411. * @type {Object}
  3412. */
  3413. _callbacks = {},
  3414. /**
  3415. * direct map of string combinations to callbacks used for trigger()
  3416. *
  3417. * @type {Object}
  3418. */
  3419. _direct_map = {},
  3420. /**
  3421. * keeps track of what level each sequence is at since multiple
  3422. * sequences can start out with the same sequence
  3423. *
  3424. * @type {Object}
  3425. */
  3426. _sequence_levels = {},
  3427. /**
  3428. * variable to store the setTimeout call
  3429. *
  3430. * @type {null|number}
  3431. */
  3432. _reset_timer,
  3433. /**
  3434. * temporary state where we will ignore the next keyup
  3435. *
  3436. * @type {boolean|string}
  3437. */
  3438. _ignore_next_keyup = false,
  3439. /**
  3440. * are we currently inside of a sequence?
  3441. * type of action ("keyup" or "keydown" or "keypress") or false
  3442. *
  3443. * @type {boolean|string}
  3444. */
  3445. _inside_sequence = false;
  3446. /**
  3447. * loop through the f keys, f1 to f19 and add them to the map
  3448. * programatically
  3449. */
  3450. for (var i = 1; i < 20; ++i) {
  3451. _MAP[111 + i] = 'f' + i;
  3452. }
  3453. /**
  3454. * loop through to map numbers on the numeric keypad
  3455. */
  3456. for (i = 0; i <= 9; ++i) {
  3457. _MAP[i + 96] = i;
  3458. }
  3459. /**
  3460. * cross browser add event method
  3461. *
  3462. * @param {Element|HTMLDocument} object
  3463. * @param {string} type
  3464. * @param {Function} callback
  3465. * @returns void
  3466. */
  3467. function _addEvent(object, type, callback) {
  3468. if (object.addEventListener) {
  3469. return object.addEventListener(type, callback, false);
  3470. }
  3471. object.attachEvent('on' + type, callback);
  3472. }
  3473. /**
  3474. * takes the event and returns the key character
  3475. *
  3476. * @param {Event} e
  3477. * @return {string}
  3478. */
  3479. function _characterFromEvent(e) {
  3480. // for keypress events we should return the character as is
  3481. if (e.type == 'keypress') {
  3482. return String.fromCharCode(e.which);
  3483. }
  3484. // for non keypress events the special maps are needed
  3485. if (_MAP[e.which]) {
  3486. return _MAP[e.which];
  3487. }
  3488. if (_KEYCODE_MAP[e.which]) {
  3489. return _KEYCODE_MAP[e.which];
  3490. }
  3491. // if it is not in the special map
  3492. return String.fromCharCode(e.which).toLowerCase();
  3493. }
  3494. /**
  3495. * should we stop this event before firing off callbacks
  3496. *
  3497. * @param {Event} e
  3498. * @return {boolean}
  3499. */
  3500. function _stop(e) {
  3501. var element = e.target || e.srcElement,
  3502. tag_name = element.tagName;
  3503. // if the element has the class "mousetrap" then no need to stop
  3504. if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
  3505. return false;
  3506. }
  3507. // stop for input, select, and textarea
  3508. return tag_name == 'INPUT' || tag_name == 'SELECT' || tag_name == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
  3509. }
  3510. /**
  3511. * checks if two arrays are equal
  3512. *
  3513. * @param {Array} modifiers1
  3514. * @param {Array} modifiers2
  3515. * @returns {boolean}
  3516. */
  3517. function _modifiersMatch(modifiers1, modifiers2) {
  3518. return modifiers1.sort().join(',') === modifiers2.sort().join(',');
  3519. }
  3520. /**
  3521. * resets all sequence counters except for the ones passed in
  3522. *
  3523. * @param {Object} do_not_reset
  3524. * @returns void
  3525. */
  3526. function _resetSequences(do_not_reset) {
  3527. do_not_reset = do_not_reset || {};
  3528. var active_sequences = false,
  3529. key;
  3530. for (key in _sequence_levels) {
  3531. if (do_not_reset[key]) {
  3532. active_sequences = true;
  3533. continue;
  3534. }
  3535. _sequence_levels[key] = 0;
  3536. }
  3537. if (!active_sequences) {
  3538. _inside_sequence = false;
  3539. }
  3540. }
  3541. /**
  3542. * finds all callbacks that match based on the keycode, modifiers,
  3543. * and action
  3544. *
  3545. * @param {string} character
  3546. * @param {Array} modifiers
  3547. * @param {string} action
  3548. * @param {boolean=} remove - should we remove any matches
  3549. * @param {string=} combination
  3550. * @returns {Array}
  3551. */
  3552. function _getMatches(character, modifiers, action, remove, combination) {
  3553. var i,
  3554. callback,
  3555. matches = [];
  3556. // if there are no events related to this keycode
  3557. if (!_callbacks[character]) {
  3558. return [];
  3559. }
  3560. // if a modifier key is coming up on its own we should allow it
  3561. if (action == 'keyup' && _isModifier(character)) {
  3562. modifiers = [character];
  3563. }
  3564. // loop through all callbacks for the key that was pressed
  3565. // and see if any of them match
  3566. for (i = 0; i < _callbacks[character].length; ++i) {
  3567. callback = _callbacks[character][i];
  3568. // if this is a sequence but it is not at the right level
  3569. // then move onto the next match
  3570. if (callback.seq && _sequence_levels[callback.seq] != callback.level) {
  3571. continue;
  3572. }
  3573. // if the action we are looking for doesn't match the action we got
  3574. // then we should keep going
  3575. if (action != callback.action) {
  3576. continue;
  3577. }
  3578. // if this is a keypress event that means that we need to only
  3579. // look at the character, otherwise check the modifiers as
  3580. // well
  3581. if (action == 'keypress' || _modifiersMatch(modifiers, callback.modifiers)) {
  3582. // remove is used so if you change your mind and call bind a
  3583. // second time with a new function the first one is overwritten
  3584. if (remove && callback.combo == combination) {
  3585. _callbacks[character].splice(i, 1);
  3586. }
  3587. matches.push(callback);
  3588. }
  3589. }
  3590. return matches;
  3591. }
  3592. /**
  3593. * takes a key event and figures out what the modifiers are
  3594. *
  3595. * @param {Event} e
  3596. * @returns {Array}
  3597. */
  3598. function _eventModifiers(e) {
  3599. var modifiers = [];
  3600. if (e.shiftKey) {
  3601. modifiers.push('shift');
  3602. }
  3603. if (e.altKey) {
  3604. modifiers.push('alt');
  3605. }
  3606. if (e.ctrlKey) {
  3607. modifiers.push('ctrl');
  3608. }
  3609. if (e.metaKey) {
  3610. modifiers.push('meta');
  3611. }
  3612. return modifiers;
  3613. }
  3614. /**
  3615. * actually calls the callback function
  3616. *
  3617. * if your callback function returns false this will use the jquery
  3618. * convention - prevent default and stop propogation on the event
  3619. *
  3620. * @param {Function} callback
  3621. * @param {Event} e
  3622. * @returns void
  3623. */
  3624. function _fireCallback(callback, e) {
  3625. if (callback(e) === false) {
  3626. if (e.preventDefault) {
  3627. e.preventDefault();
  3628. }
  3629. if (e.stopPropagation) {
  3630. e.stopPropagation();
  3631. }
  3632. e.returnValue = false;
  3633. e.cancelBubble = true;
  3634. }
  3635. }
  3636. /**
  3637. * handles a character key event
  3638. *
  3639. * @param {string} character
  3640. * @param {Event} e
  3641. * @returns void
  3642. */
  3643. function _handleCharacter(character, e) {
  3644. // if this event should not happen stop here
  3645. if (_stop(e)) {
  3646. return;
  3647. }
  3648. var callbacks = _getMatches(character, _eventModifiers(e), e.type),
  3649. i,
  3650. do_not_reset = {},
  3651. processed_sequence_callback = false;
  3652. // loop through matching callbacks for this key event
  3653. for (i = 0; i < callbacks.length; ++i) {
  3654. // fire for all sequence callbacks
  3655. // this is because if for example you have multiple sequences
  3656. // bound such as "g i" and "g t" they both need to fire the
  3657. // callback for matching g cause otherwise you can only ever
  3658. // match the first one
  3659. if (callbacks[i].seq) {
  3660. processed_sequence_callback = true;
  3661. // keep a list of which sequences were matches for later
  3662. do_not_reset[callbacks[i].seq] = 1;
  3663. _fireCallback(callbacks[i].callback, e);
  3664. continue;
  3665. }
  3666. // if there were no sequence matches but we are still here
  3667. // that means this is a regular match so we should fire that
  3668. if (!processed_sequence_callback && !_inside_sequence) {
  3669. _fireCallback(callbacks[i].callback, e);
  3670. }
  3671. }
  3672. // if you are inside of a sequence and the key you are pressing
  3673. // is not a modifier key then we should reset all sequences
  3674. // that were not matched by this key event
  3675. if (e.type == _inside_sequence && !_isModifier(character)) {
  3676. _resetSequences(do_not_reset);
  3677. }
  3678. }
  3679. /**
  3680. * handles a keydown event
  3681. *
  3682. * @param {Event} e
  3683. * @returns void
  3684. */
  3685. function _handleKey(e) {
  3686. // normalize e.which for key events
  3687. // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
  3688. e.which = typeof e.which == "number" ? e.which : e.keyCode;
  3689. var character = _characterFromEvent(e);
  3690. // no character found then stop
  3691. if (!character) {
  3692. return;
  3693. }
  3694. if (e.type == 'keyup' && _ignore_next_keyup == character) {
  3695. _ignore_next_keyup = false;
  3696. return;
  3697. }
  3698. _handleCharacter(character, e);
  3699. }
  3700. /**
  3701. * determines if the keycode specified is a modifier key or not
  3702. *
  3703. * @param {string} key
  3704. * @returns {boolean}
  3705. */
  3706. function _isModifier(key) {
  3707. return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
  3708. }
  3709. /**
  3710. * called to set a 1 second timeout on the specified sequence
  3711. *
  3712. * this is so after each key press in the sequence you have 1 second
  3713. * to press the next key before you have to start over
  3714. *
  3715. * @returns void
  3716. */
  3717. function _resetSequenceTimer() {
  3718. clearTimeout(_reset_timer);
  3719. _reset_timer = setTimeout(_resetSequences, 1000);
  3720. }
  3721. /**
  3722. * reverses the map lookup so that we can look for specific keys
  3723. * to see what can and can't use keypress
  3724. *
  3725. * @return {Object}
  3726. */
  3727. function _getReverseMap() {
  3728. if (!_REVERSE_MAP) {
  3729. _REVERSE_MAP = {};
  3730. for (var key in _MAP) {
  3731. // pull out the numeric keypad from here cause keypress should
  3732. // be able to detect the keys from the character
  3733. if (key > 95 && key < 112) {
  3734. continue;
  3735. }
  3736. if (_MAP.hasOwnProperty(key)) {
  3737. _REVERSE_MAP[_MAP[key]] = key;
  3738. }
  3739. }
  3740. }
  3741. return _REVERSE_MAP;
  3742. }
  3743. /**
  3744. * picks the best action based on the key combination
  3745. *
  3746. * @param {string} key - character for key
  3747. * @param {Array} modifiers
  3748. * @param {string=} action passed in
  3749. */
  3750. function _pickBestAction(key, modifiers, action) {
  3751. // if no action was picked in we should try to pick the one
  3752. // that we think would work best for this key
  3753. if (!action) {
  3754. action = _getReverseMap()[key] ? 'keydown' : 'keypress';
  3755. }
  3756. // modifier keys don't work as expected with keypress,
  3757. // switch to keydown
  3758. if (action == 'keypress' && modifiers.length) {
  3759. action = 'keydown';
  3760. }
  3761. return action;
  3762. }
  3763. /**
  3764. * binds a key sequence to an event
  3765. *
  3766. * @param {string} combo - combo specified in bind call
  3767. * @param {Array} keys
  3768. * @param {Function} callback
  3769. * @param {string=} action
  3770. * @returns void
  3771. */
  3772. function _bindSequence(combo, keys, callback, action) {
  3773. // start off by adding a sequence level record for this combination
  3774. // and setting the level to 0
  3775. _sequence_levels[combo] = 0;
  3776. // if there is no action pick the best one for the first key
  3777. // in the sequence
  3778. if (!action) {
  3779. action = _pickBestAction(keys[0], []);
  3780. }
  3781. /**
  3782. * callback to increase the sequence level for this sequence and reset
  3783. * all other sequences that were active
  3784. *
  3785. * @param {Event} e
  3786. * @returns void
  3787. */
  3788. var _increaseSequence = function(e) {
  3789. _inside_sequence = action;
  3790. ++_sequence_levels[combo];
  3791. _resetSequenceTimer();
  3792. },
  3793. /**
  3794. * wraps the specified callback inside of another function in order
  3795. * to reset all sequence counters as soon as this sequence is done
  3796. *
  3797. * @param {Event} e
  3798. * @returns void
  3799. */
  3800. _callbackAndReset = function(e) {
  3801. _fireCallback(callback, e);
  3802. // we should ignore the next key up if the action is key down
  3803. // or keypress. this is so if you finish a sequence and
  3804. // release the key the final key will not trigger a keyup
  3805. if (action !== 'keyup') {
  3806. _ignore_next_keyup = _characterFromEvent(e);
  3807. }
  3808. // weird race condition if a sequence ends with the key
  3809. // another sequence begins with
  3810. setTimeout(_resetSequences, 10);
  3811. },
  3812. i;
  3813. // loop through keys one at a time and bind the appropriate callback
  3814. // function. for any key leading up to the final one it should
  3815. // increase the sequence. after the final, it should reset all sequences
  3816. for (i = 0; i < keys.length; ++i) {
  3817. _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
  3818. }
  3819. }
  3820. /**
  3821. * binds a single keyboard combination
  3822. *
  3823. * @param {string} combination
  3824. * @param {Function} callback
  3825. * @param {string=} action
  3826. * @param {string=} sequence_name - name of sequence if part of sequence
  3827. * @param {number=} level - what part of the sequence the command is
  3828. * @returns void
  3829. */
  3830. function _bindSingle(combination, callback, action, sequence_name, level) {
  3831. // make sure multiple spaces in a row become a single space
  3832. combination = combination.replace(/\s+/g, ' ');
  3833. var sequence = combination.split(' '),
  3834. i,
  3835. key,
  3836. keys,
  3837. modifiers = [];
  3838. // if this pattern is a sequence of keys then run through this method
  3839. // to reprocess each pattern one key at a time
  3840. if (sequence.length > 1) {
  3841. return _bindSequence(combination, sequence, callback, action);
  3842. }
  3843. // take the keys from this pattern and figure out what the actual
  3844. // pattern is all about
  3845. keys = combination === '+' ? ['+'] : combination.split('+');
  3846. for (i = 0; i < keys.length; ++i) {
  3847. key = keys[i];
  3848. // normalize key names
  3849. if (_SPECIAL_ALIASES[key]) {
  3850. key = _SPECIAL_ALIASES[key];
  3851. }
  3852. // if this is not a keypress event then we should
  3853. // be smart about using shift keys
  3854. // this will only work for US keyboards however
  3855. if (action && action != 'keypress' && _SHIFT_MAP[key]) {
  3856. key = _SHIFT_MAP[key];
  3857. modifiers.push('shift');
  3858. }
  3859. // if this key is a modifier then add it to the list of modifiers
  3860. if (_isModifier(key)) {
  3861. modifiers.push(key);
  3862. }
  3863. }
  3864. // depending on what the key combination is
  3865. // we will try to pick the best event for it
  3866. action = _pickBestAction(key, modifiers, action);
  3867. // make sure to initialize array if this is the first time
  3868. // a callback is added for this key
  3869. if (!_callbacks[key]) {
  3870. _callbacks[key] = [];
  3871. }
  3872. // remove an existing match if there is one
  3873. _getMatches(key, modifiers, action, !sequence_name, combination);
  3874. // add this call back to the array
  3875. // if it is a sequence put it at the beginning
  3876. // if not put it at the end
  3877. //
  3878. // this is important because the way these are processed expects
  3879. // the sequence ones to come first
  3880. _callbacks[key][sequence_name ? 'unshift' : 'push']({
  3881. callback: callback,
  3882. modifiers: modifiers,
  3883. action: action,
  3884. seq: sequence_name,
  3885. level: level,
  3886. combo: combination
  3887. });
  3888. }
  3889. /**
  3890. * binds multiple combinations to the same callback
  3891. *
  3892. * @param {Array} combinations
  3893. * @param {Function} callback
  3894. * @param {string|undefined} action
  3895. * @returns void
  3896. */
  3897. function _bindMultiple(combinations, callback, action) {
  3898. for (var i = 0; i < combinations.length; ++i) {
  3899. _bindSingle(combinations[i], callback, action);
  3900. }
  3901. }
  3902. // start!
  3903. _addEvent(document, 'keypress', _handleKey);
  3904. _addEvent(document, 'keydown', _handleKey);
  3905. _addEvent(document, 'keyup', _handleKey);
  3906. var mousetrap = {
  3907. /**
  3908. * binds an event to mousetrap
  3909. *
  3910. * can be a single key, a combination of keys separated with +,
  3911. * a comma separated list of keys, an array of keys, or
  3912. * a sequence of keys separated by spaces
  3913. *
  3914. * be sure to list the modifier keys first to make sure that the
  3915. * correct key ends up getting bound (the last key in the pattern)
  3916. *
  3917. * @param {string|Array} keys
  3918. * @param {Function} callback
  3919. * @param {string=} action - 'keypress', 'keydown', or 'keyup'
  3920. * @returns void
  3921. */
  3922. bind: function(keys, callback, action) {
  3923. _bindMultiple(keys instanceof Array ? keys : [keys], callback, action);
  3924. _direct_map[keys + ':' + action] = callback;
  3925. return this;
  3926. },
  3927. /**
  3928. * unbinds an event to mousetrap
  3929. *
  3930. * the unbinding sets the callback function of the specified key combo
  3931. * to an empty function and deletes the corresponding key in the
  3932. * _direct_map dict.
  3933. *
  3934. * the keycombo+action has to be exactly the same as
  3935. * it was defined in the bind method
  3936. *
  3937. * TODO: actually remove this from the _callbacks dictionary instead
  3938. * of binding an empty function
  3939. *
  3940. * @param {string|Array} keys
  3941. * @param {string} action
  3942. * @returns void
  3943. */
  3944. unbind: function(keys, action) {
  3945. if (_direct_map[keys + ':' + action]) {
  3946. delete _direct_map[keys + ':' + action];
  3947. this.bind(keys, function() {}, action);
  3948. }
  3949. return this;
  3950. },
  3951. /**
  3952. * triggers an event that has already been bound
  3953. *
  3954. * @param {string} keys
  3955. * @param {string=} action
  3956. * @returns void
  3957. */
  3958. trigger: function(keys, action) {
  3959. _direct_map[keys + ':' + action]();
  3960. return this;
  3961. },
  3962. /**
  3963. * resets the library back to its initial state. this is useful
  3964. * if you want to clear out the current keyboard shortcuts and bind
  3965. * new ones - for example if you switch to another page
  3966. *
  3967. * @returns void
  3968. */
  3969. reset: function() {
  3970. _callbacks = {};
  3971. _direct_map = {};
  3972. return this;
  3973. }
  3974. };
  3975. module.exports = mousetrap;
  3976. },{}],4:[function(require,module,exports){
  3977. /**
  3978. * vis.js module imports
  3979. */
  3980. // Try to load dependencies from the global window object.
  3981. // If not available there, load via require.
  3982. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  3983. var Hammer;
  3984. if (typeof window !== 'undefined') {
  3985. // load hammer.js only when running in a browser (where window is available)
  3986. Hammer = window['Hammer'] || require('hammerjs');
  3987. }
  3988. else {
  3989. Hammer = function () {
  3990. throw Error('hammer.js is only available in a browser, not in node.js.');
  3991. }
  3992. }
  3993. var mouseTrap;
  3994. if (typeof window !== 'undefined') {
  3995. // load mousetrap.js only when running in a browser (where window is available)
  3996. mouseTrap = window['mouseTrap'] || require('mouseTrap');
  3997. }
  3998. else {
  3999. mouseTrap = function () {
  4000. throw Error('mouseTrap is only available in a browser, not in node.js.');
  4001. }
  4002. }
  4003. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  4004. // it here in that case.
  4005. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  4006. if(!Array.prototype.indexOf) {
  4007. Array.prototype.indexOf = function(obj){
  4008. for(var i = 0; i < this.length; i++){
  4009. if(this[i] == obj){
  4010. return i;
  4011. }
  4012. }
  4013. return -1;
  4014. };
  4015. try {
  4016. console.log("Warning: Ancient browser detected. Please update your browser");
  4017. }
  4018. catch (err) {
  4019. }
  4020. }
  4021. // Internet Explorer 8 and older does not support Array.forEach, so we define
  4022. // it here in that case.
  4023. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  4024. if (!Array.prototype.forEach) {
  4025. Array.prototype.forEach = function(fn, scope) {
  4026. for(var i = 0, len = this.length; i < len; ++i) {
  4027. fn.call(scope || this, this[i], i, this);
  4028. }
  4029. }
  4030. }
  4031. // Internet Explorer 8 and older does not support Array.map, so we define it
  4032. // here in that case.
  4033. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  4034. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  4035. // Reference: http://es5.github.com/#x15.4.4.19
  4036. if (!Array.prototype.map) {
  4037. Array.prototype.map = function(callback, thisArg) {
  4038. var T, A, k;
  4039. if (this == null) {
  4040. throw new TypeError(" this is null or not defined");
  4041. }
  4042. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  4043. var O = Object(this);
  4044. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  4045. // 3. Let len be ToUint32(lenValue).
  4046. var len = O.length >>> 0;
  4047. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  4048. // See: http://es5.github.com/#x9.11
  4049. if (typeof callback !== "function") {
  4050. throw new TypeError(callback + " is not a function");
  4051. }
  4052. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  4053. if (thisArg) {
  4054. T = thisArg;
  4055. }
  4056. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  4057. // the standard built-in constructor with that name and len is the value of len.
  4058. A = new Array(len);
  4059. // 7. Let k be 0
  4060. k = 0;
  4061. // 8. Repeat, while k < len
  4062. while(k < len) {
  4063. var kValue, mappedValue;
  4064. // a. Let Pk be ToString(k).
  4065. // This is implicit for LHS operands of the in operator
  4066. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  4067. // This step can be combined with c
  4068. // c. If kPresent is true, then
  4069. if (k in O) {
  4070. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  4071. kValue = O[ k ];
  4072. // ii. Let mappedValue be the result of calling the Call internal method of callback
  4073. // with T as the this value and argument list containing kValue, k, and O.
  4074. mappedValue = callback.call(T, kValue, k, O);
  4075. // iii. Call the DefineOwnProperty internal method of A with arguments
  4076. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  4077. // and false.
  4078. // In browsers that support Object.defineProperty, use the following:
  4079. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  4080. // For best browser support, use the following:
  4081. A[ k ] = mappedValue;
  4082. }
  4083. // d. Increase k by 1.
  4084. k++;
  4085. }
  4086. // 9. return A
  4087. return A;
  4088. };
  4089. }
  4090. // Internet Explorer 8 and older does not support Array.filter, so we define it
  4091. // here in that case.
  4092. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  4093. if (!Array.prototype.filter) {
  4094. Array.prototype.filter = function(fun /*, thisp */) {
  4095. "use strict";
  4096. if (this == null) {
  4097. throw new TypeError();
  4098. }
  4099. var t = Object(this);
  4100. var len = t.length >>> 0;
  4101. if (typeof fun != "function") {
  4102. throw new TypeError();
  4103. }
  4104. var res = [];
  4105. var thisp = arguments[1];
  4106. for (var i = 0; i < len; i++) {
  4107. if (i in t) {
  4108. var val = t[i]; // in case fun mutates this
  4109. if (fun.call(thisp, val, i, t))
  4110. res.push(val);
  4111. }
  4112. }
  4113. return res;
  4114. };
  4115. }
  4116. // Internet Explorer 8 and older does not support Object.keys, so we define it
  4117. // here in that case.
  4118. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  4119. if (!Object.keys) {
  4120. Object.keys = (function () {
  4121. var hasOwnProperty = Object.prototype.hasOwnProperty,
  4122. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  4123. dontEnums = [
  4124. 'toString',
  4125. 'toLocaleString',
  4126. 'valueOf',
  4127. 'hasOwnProperty',
  4128. 'isPrototypeOf',
  4129. 'propertyIsEnumerable',
  4130. 'constructor'
  4131. ],
  4132. dontEnumsLength = dontEnums.length;
  4133. return function (obj) {
  4134. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  4135. throw new TypeError('Object.keys called on non-object');
  4136. }
  4137. var result = [];
  4138. for (var prop in obj) {
  4139. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  4140. }
  4141. if (hasDontEnumBug) {
  4142. for (var i=0; i < dontEnumsLength; i++) {
  4143. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  4144. }
  4145. }
  4146. return result;
  4147. }
  4148. })()
  4149. }
  4150. // Internet Explorer 8 and older does not support Array.isArray,
  4151. // so we define it here in that case.
  4152. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  4153. if(!Array.isArray) {
  4154. Array.isArray = function (vArg) {
  4155. return Object.prototype.toString.call(vArg) === "[object Array]";
  4156. };
  4157. }
  4158. // Internet Explorer 8 and older does not support Function.bind,
  4159. // so we define it here in that case.
  4160. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  4161. if (!Function.prototype.bind) {
  4162. Function.prototype.bind = function (oThis) {
  4163. if (typeof this !== "function") {
  4164. // closest thing possible to the ECMAScript 5 internal IsCallable function
  4165. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  4166. }
  4167. var aArgs = Array.prototype.slice.call(arguments, 1),
  4168. fToBind = this,
  4169. fNOP = function () {},
  4170. fBound = function () {
  4171. return fToBind.apply(this instanceof fNOP && oThis
  4172. ? this
  4173. : oThis,
  4174. aArgs.concat(Array.prototype.slice.call(arguments)));
  4175. };
  4176. fNOP.prototype = this.prototype;
  4177. fBound.prototype = new fNOP();
  4178. return fBound;
  4179. };
  4180. }
  4181. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  4182. if (!Object.create) {
  4183. Object.create = function (o) {
  4184. if (arguments.length > 1) {
  4185. throw new Error('Object.create implementation only accepts the first parameter.');
  4186. }
  4187. function F() {}
  4188. F.prototype = o;
  4189. return new F();
  4190. };
  4191. }
  4192. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  4193. if (!Function.prototype.bind) {
  4194. Function.prototype.bind = function (oThis) {
  4195. if (typeof this !== "function") {
  4196. // closest thing possible to the ECMAScript 5 internal IsCallable function
  4197. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  4198. }
  4199. var aArgs = Array.prototype.slice.call(arguments, 1),
  4200. fToBind = this,
  4201. fNOP = function () {},
  4202. fBound = function () {
  4203. return fToBind.apply(this instanceof fNOP && oThis
  4204. ? this
  4205. : oThis,
  4206. aArgs.concat(Array.prototype.slice.call(arguments)));
  4207. };
  4208. fNOP.prototype = this.prototype;
  4209. fBound.prototype = new fNOP();
  4210. return fBound;
  4211. };
  4212. }
  4213. /**
  4214. * utility functions
  4215. */
  4216. var util = {};
  4217. /**
  4218. * Test whether given object is a number
  4219. * @param {*} object
  4220. * @return {Boolean} isNumber
  4221. */
  4222. util.isNumber = function isNumber(object) {
  4223. return (object instanceof Number || typeof object == 'number');
  4224. };
  4225. /**
  4226. * Test whether given object is a string
  4227. * @param {*} object
  4228. * @return {Boolean} isString
  4229. */
  4230. util.isString = function isString(object) {
  4231. return (object instanceof String || typeof object == 'string');
  4232. };
  4233. /**
  4234. * Test whether given object is a Date, or a String containing a Date
  4235. * @param {Date | String} object
  4236. * @return {Boolean} isDate
  4237. */
  4238. util.isDate = function isDate(object) {
  4239. if (object instanceof Date) {
  4240. return true;
  4241. }
  4242. else if (util.isString(object)) {
  4243. // test whether this string contains a date
  4244. var match = ASPDateRegex.exec(object);
  4245. if (match) {
  4246. return true;
  4247. }
  4248. else if (!isNaN(Date.parse(object))) {
  4249. return true;
  4250. }
  4251. }
  4252. return false;
  4253. };
  4254. /**
  4255. * Test whether given object is an instance of google.visualization.DataTable
  4256. * @param {*} object
  4257. * @return {Boolean} isDataTable
  4258. */
  4259. util.isDataTable = function isDataTable(object) {
  4260. return (typeof (google) !== 'undefined') &&
  4261. (google.visualization) &&
  4262. (google.visualization.DataTable) &&
  4263. (object instanceof google.visualization.DataTable);
  4264. };
  4265. /**
  4266. * Create a semi UUID
  4267. * source: http://stackoverflow.com/a/105074/1262753
  4268. * @return {String} uuid
  4269. */
  4270. util.randomUUID = function randomUUID () {
  4271. var S4 = function () {
  4272. return Math.floor(
  4273. Math.random() * 0x10000 /* 65536 */
  4274. ).toString(16);
  4275. };
  4276. return (
  4277. S4() + S4() + '-' +
  4278. S4() + '-' +
  4279. S4() + '-' +
  4280. S4() + '-' +
  4281. S4() + S4() + S4()
  4282. );
  4283. };
  4284. /**
  4285. * Extend object a with the properties of object b or a series of objects
  4286. * Only properties with defined values are copied
  4287. * @param {Object} a
  4288. * @param {... Object} b
  4289. * @return {Object} a
  4290. */
  4291. util.extend = function (a, b) {
  4292. for (var i = 1, len = arguments.length; i < len; i++) {
  4293. var other = arguments[i];
  4294. for (var prop in other) {
  4295. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  4296. a[prop] = other[prop];
  4297. }
  4298. }
  4299. }
  4300. return a;
  4301. };
  4302. /**
  4303. * Convert an object to another type
  4304. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  4305. * @param {String | undefined} type Name of the type. Available types:
  4306. * 'Boolean', 'Number', 'String',
  4307. * 'Date', 'Moment', ISODate', 'ASPDate'.
  4308. * @return {*} object
  4309. * @throws Error
  4310. */
  4311. util.convert = function convert(object, type) {
  4312. var match;
  4313. if (object === undefined) {
  4314. return undefined;
  4315. }
  4316. if (object === null) {
  4317. return null;
  4318. }
  4319. if (!type) {
  4320. return object;
  4321. }
  4322. if (!(typeof type === 'string') && !(type instanceof String)) {
  4323. throw new Error('Type must be a string');
  4324. }
  4325. //noinspection FallthroughInSwitchStatementJS
  4326. switch (type) {
  4327. case 'boolean':
  4328. case 'Boolean':
  4329. return Boolean(object);
  4330. case 'number':
  4331. case 'Number':
  4332. return Number(object.valueOf());
  4333. case 'string':
  4334. case 'String':
  4335. return String(object);
  4336. case 'Date':
  4337. if (util.isNumber(object)) {
  4338. return new Date(object);
  4339. }
  4340. if (object instanceof Date) {
  4341. return new Date(object.valueOf());
  4342. }
  4343. else if (moment.isMoment(object)) {
  4344. return new Date(object.valueOf());
  4345. }
  4346. if (util.isString(object)) {
  4347. match = ASPDateRegex.exec(object);
  4348. if (match) {
  4349. // object is an ASP date
  4350. return new Date(Number(match[1])); // parse number
  4351. }
  4352. else {
  4353. return moment(object).toDate(); // parse string
  4354. }
  4355. }
  4356. else {
  4357. throw new Error(
  4358. 'Cannot convert object of type ' + util.getType(object) +
  4359. ' to type Date');
  4360. }
  4361. case 'Moment':
  4362. if (util.isNumber(object)) {
  4363. return moment(object);
  4364. }
  4365. if (object instanceof Date) {
  4366. return moment(object.valueOf());
  4367. }
  4368. else if (moment.isMoment(object)) {
  4369. return moment(object);
  4370. }
  4371. if (util.isString(object)) {
  4372. match = ASPDateRegex.exec(object);
  4373. if (match) {
  4374. // object is an ASP date
  4375. return moment(Number(match[1])); // parse number
  4376. }
  4377. else {
  4378. return moment(object); // parse string
  4379. }
  4380. }
  4381. else {
  4382. throw new Error(
  4383. 'Cannot convert object of type ' + util.getType(object) +
  4384. ' to type Date');
  4385. }
  4386. case 'ISODate':
  4387. if (util.isNumber(object)) {
  4388. return new Date(object);
  4389. }
  4390. else if (object instanceof Date) {
  4391. return object.toISOString();
  4392. }
  4393. else if (moment.isMoment(object)) {
  4394. return object.toDate().toISOString();
  4395. }
  4396. else if (util.isString(object)) {
  4397. match = ASPDateRegex.exec(object);
  4398. if (match) {
  4399. // object is an ASP date
  4400. return new Date(Number(match[1])).toISOString(); // parse number
  4401. }
  4402. else {
  4403. return new Date(object).toISOString(); // parse string
  4404. }
  4405. }
  4406. else {
  4407. throw new Error(
  4408. 'Cannot convert object of type ' + util.getType(object) +
  4409. ' to type ISODate');
  4410. }
  4411. case 'ASPDate':
  4412. if (util.isNumber(object)) {
  4413. return '/Date(' + object + ')/';
  4414. }
  4415. else if (object instanceof Date) {
  4416. return '/Date(' + object.valueOf() + ')/';
  4417. }
  4418. else if (util.isString(object)) {
  4419. match = ASPDateRegex.exec(object);
  4420. var value;
  4421. if (match) {
  4422. // object is an ASP date
  4423. value = new Date(Number(match[1])).valueOf(); // parse number
  4424. }
  4425. else {
  4426. value = new Date(object).valueOf(); // parse string
  4427. }
  4428. return '/Date(' + value + ')/';
  4429. }
  4430. else {
  4431. throw new Error(
  4432. 'Cannot convert object of type ' + util.getType(object) +
  4433. ' to type ASPDate');
  4434. }
  4435. default:
  4436. throw new Error('Cannot convert object of type ' + util.getType(object) +
  4437. ' to type "' + type + '"');
  4438. }
  4439. };
  4440. // parse ASP.Net Date pattern,
  4441. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  4442. // code from http://momentjs.com/
  4443. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  4444. /**
  4445. * Get the type of an object, for example util.getType([]) returns 'Array'
  4446. * @param {*} object
  4447. * @return {String} type
  4448. */
  4449. util.getType = function getType(object) {
  4450. var type = typeof object;
  4451. if (type == 'object') {
  4452. if (object == null) {
  4453. return 'null';
  4454. }
  4455. if (object instanceof Boolean) {
  4456. return 'Boolean';
  4457. }
  4458. if (object instanceof Number) {
  4459. return 'Number';
  4460. }
  4461. if (object instanceof String) {
  4462. return 'String';
  4463. }
  4464. if (object instanceof Array) {
  4465. return 'Array';
  4466. }
  4467. if (object instanceof Date) {
  4468. return 'Date';
  4469. }
  4470. return 'Object';
  4471. }
  4472. else if (type == 'number') {
  4473. return 'Number';
  4474. }
  4475. else if (type == 'boolean') {
  4476. return 'Boolean';
  4477. }
  4478. else if (type == 'string') {
  4479. return 'String';
  4480. }
  4481. return type;
  4482. };
  4483. /**
  4484. * Retrieve the absolute left value of a DOM element
  4485. * @param {Element} elem A dom element, for example a div
  4486. * @return {number} left The absolute left position of this element
  4487. * in the browser page.
  4488. */
  4489. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  4490. var doc = document.documentElement;
  4491. var body = document.body;
  4492. var left = elem.offsetLeft;
  4493. var e = elem.offsetParent;
  4494. while (e != null && e != body && e != doc) {
  4495. left += e.offsetLeft;
  4496. left -= e.scrollLeft;
  4497. e = e.offsetParent;
  4498. }
  4499. return left;
  4500. };
  4501. /**
  4502. * Retrieve the absolute top value of a DOM element
  4503. * @param {Element} elem A dom element, for example a div
  4504. * @return {number} top The absolute top position of this element
  4505. * in the browser page.
  4506. */
  4507. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  4508. var doc = document.documentElement;
  4509. var body = document.body;
  4510. var top = elem.offsetTop;
  4511. var e = elem.offsetParent;
  4512. while (e != null && e != body && e != doc) {
  4513. top += e.offsetTop;
  4514. top -= e.scrollTop;
  4515. e = e.offsetParent;
  4516. }
  4517. return top;
  4518. };
  4519. /**
  4520. * Get the absolute, vertical mouse position from an event.
  4521. * @param {Event} event
  4522. * @return {Number} pageY
  4523. */
  4524. util.getPageY = function getPageY (event) {
  4525. if ('pageY' in event) {
  4526. return event.pageY;
  4527. }
  4528. else {
  4529. var clientY;
  4530. if (('targetTouches' in event) && event.targetTouches.length) {
  4531. clientY = event.targetTouches[0].clientY;
  4532. }
  4533. else {
  4534. clientY = event.clientY;
  4535. }
  4536. var doc = document.documentElement;
  4537. var body = document.body;
  4538. return clientY +
  4539. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  4540. ( doc && doc.clientTop || body && body.clientTop || 0 );
  4541. }
  4542. };
  4543. /**
  4544. * Get the absolute, horizontal mouse position from an event.
  4545. * @param {Event} event
  4546. * @return {Number} pageX
  4547. */
  4548. util.getPageX = function getPageX (event) {
  4549. if ('pageY' in event) {
  4550. return event.pageX;
  4551. }
  4552. else {
  4553. var clientX;
  4554. if (('targetTouches' in event) && event.targetTouches.length) {
  4555. clientX = event.targetTouches[0].clientX;
  4556. }
  4557. else {
  4558. clientX = event.clientX;
  4559. }
  4560. var doc = document.documentElement;
  4561. var body = document.body;
  4562. return clientX +
  4563. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  4564. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  4565. }
  4566. };
  4567. /**
  4568. * add a className to the given elements style
  4569. * @param {Element} elem
  4570. * @param {String} className
  4571. */
  4572. util.addClassName = function addClassName(elem, className) {
  4573. var classes = elem.className.split(' ');
  4574. if (classes.indexOf(className) == -1) {
  4575. classes.push(className); // add the class to the array
  4576. elem.className = classes.join(' ');
  4577. }
  4578. };
  4579. /**
  4580. * add a className to the given elements style
  4581. * @param {Element} elem
  4582. * @param {String} className
  4583. */
  4584. util.removeClassName = function removeClassname(elem, className) {
  4585. var classes = elem.className.split(' ');
  4586. var index = classes.indexOf(className);
  4587. if (index != -1) {
  4588. classes.splice(index, 1); // remove the class from the array
  4589. elem.className = classes.join(' ');
  4590. }
  4591. };
  4592. /**
  4593. * For each method for both arrays and objects.
  4594. * In case of an array, the built-in Array.forEach() is applied.
  4595. * In case of an Object, the method loops over all properties of the object.
  4596. * @param {Object | Array} object An Object or Array
  4597. * @param {function} callback Callback method, called for each item in
  4598. * the object or array with three parameters:
  4599. * callback(value, index, object)
  4600. */
  4601. util.forEach = function forEach (object, callback) {
  4602. var i,
  4603. len;
  4604. if (object instanceof Array) {
  4605. // array
  4606. for (i = 0, len = object.length; i < len; i++) {
  4607. callback(object[i], i, object);
  4608. }
  4609. }
  4610. else {
  4611. // object
  4612. for (i in object) {
  4613. if (object.hasOwnProperty(i)) {
  4614. callback(object[i], i, object);
  4615. }
  4616. }
  4617. }
  4618. };
  4619. /**
  4620. * Update a property in an object
  4621. * @param {Object} object
  4622. * @param {String} key
  4623. * @param {*} value
  4624. * @return {Boolean} changed
  4625. */
  4626. util.updateProperty = function updateProp (object, key, value) {
  4627. if (object[key] !== value) {
  4628. object[key] = value;
  4629. return true;
  4630. }
  4631. else {
  4632. return false;
  4633. }
  4634. };
  4635. /**
  4636. * Add and event listener. Works for all browsers
  4637. * @param {Element} element An html element
  4638. * @param {string} action The action, for example "click",
  4639. * without the prefix "on"
  4640. * @param {function} listener The callback function to be executed
  4641. * @param {boolean} [useCapture]
  4642. */
  4643. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  4644. if (element.addEventListener) {
  4645. if (useCapture === undefined)
  4646. useCapture = false;
  4647. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  4648. action = "DOMMouseScroll"; // For Firefox
  4649. }
  4650. element.addEventListener(action, listener, useCapture);
  4651. } else {
  4652. element.attachEvent("on" + action, listener); // IE browsers
  4653. }
  4654. };
  4655. /**
  4656. * Remove an event listener from an element
  4657. * @param {Element} element An html dom element
  4658. * @param {string} action The name of the event, for example "mousedown"
  4659. * @param {function} listener The listener function
  4660. * @param {boolean} [useCapture]
  4661. */
  4662. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  4663. if (element.removeEventListener) {
  4664. // non-IE browsers
  4665. if (useCapture === undefined)
  4666. useCapture = false;
  4667. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  4668. action = "DOMMouseScroll"; // For Firefox
  4669. }
  4670. element.removeEventListener(action, listener, useCapture);
  4671. } else {
  4672. // IE browsers
  4673. element.detachEvent("on" + action, listener);
  4674. }
  4675. };
  4676. /**
  4677. * Get HTML element which is the target of the event
  4678. * @param {Event} event
  4679. * @return {Element} target element
  4680. */
  4681. util.getTarget = function getTarget(event) {
  4682. // code from http://www.quirksmode.org/js/events_properties.html
  4683. if (!event) {
  4684. event = window.event;
  4685. }
  4686. var target;
  4687. if (event.target) {
  4688. target = event.target;
  4689. }
  4690. else if (event.srcElement) {
  4691. target = event.srcElement;
  4692. }
  4693. if (target.nodeType != undefined && target.nodeType == 3) {
  4694. // defeat Safari bug
  4695. target = target.parentNode;
  4696. }
  4697. return target;
  4698. };
  4699. /**
  4700. * Stop event propagation
  4701. */
  4702. util.stopPropagation = function stopPropagation(event) {
  4703. if (!event)
  4704. event = window.event;
  4705. if (event.stopPropagation) {
  4706. event.stopPropagation(); // non-IE browsers
  4707. }
  4708. else {
  4709. event.cancelBubble = true; // IE browsers
  4710. }
  4711. };
  4712. /**
  4713. * Fake a hammer.js gesture. Event can be a ScrollEvent or MouseMoveEvent
  4714. * @param {Element} element
  4715. * @param {Event} event
  4716. */
  4717. util.fakeGesture = function fakeGesture (element, event) {
  4718. var eventType = null;
  4719. // for hammer.js 1.0.5
  4720. return Hammer.event.collectEventData(this, eventType, event);
  4721. // for hammer.js 1.0.6
  4722. //var touches = Hammer.event.getTouchList(event, eventType);
  4723. //return Hammer.event.collectEventData(this, eventType, touches, event);
  4724. };
  4725. /**
  4726. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  4727. */
  4728. util.preventDefault = function preventDefault (event) {
  4729. if (!event)
  4730. event = window.event;
  4731. if (event.preventDefault) {
  4732. event.preventDefault(); // non-IE browsers
  4733. }
  4734. else {
  4735. event.returnValue = false; // IE browsers
  4736. }
  4737. };
  4738. util.option = {};
  4739. /**
  4740. * Convert a value into a boolean
  4741. * @param {Boolean | function | undefined} value
  4742. * @param {Boolean} [defaultValue]
  4743. * @returns {Boolean} bool
  4744. */
  4745. util.option.asBoolean = function (value, defaultValue) {
  4746. if (typeof value == 'function') {
  4747. value = value();
  4748. }
  4749. if (value != null) {
  4750. return (value != false);
  4751. }
  4752. return defaultValue || null;
  4753. };
  4754. /**
  4755. * Convert a value into a number
  4756. * @param {Boolean | function | undefined} value
  4757. * @param {Number} [defaultValue]
  4758. * @returns {Number} number
  4759. */
  4760. util.option.asNumber = function (value, defaultValue) {
  4761. if (typeof value == 'function') {
  4762. value = value();
  4763. }
  4764. if (value != null) {
  4765. return Number(value) || defaultValue || null;
  4766. }
  4767. return defaultValue || null;
  4768. };
  4769. /**
  4770. * Convert a value into a string
  4771. * @param {String | function | undefined} value
  4772. * @param {String} [defaultValue]
  4773. * @returns {String} str
  4774. */
  4775. util.option.asString = function (value, defaultValue) {
  4776. if (typeof value == 'function') {
  4777. value = value();
  4778. }
  4779. if (value != null) {
  4780. return String(value);
  4781. }
  4782. return defaultValue || null;
  4783. };
  4784. /**
  4785. * Convert a size or location into a string with pixels or a percentage
  4786. * @param {String | Number | function | undefined} value
  4787. * @param {String} [defaultValue]
  4788. * @returns {String} size
  4789. */
  4790. util.option.asSize = function (value, defaultValue) {
  4791. if (typeof value == 'function') {
  4792. value = value();
  4793. }
  4794. if (util.isString(value)) {
  4795. return value;
  4796. }
  4797. else if (util.isNumber(value)) {
  4798. return value + 'px';
  4799. }
  4800. else {
  4801. return defaultValue || null;
  4802. }
  4803. };
  4804. /**
  4805. * Convert a value into a DOM element
  4806. * @param {HTMLElement | function | undefined} value
  4807. * @param {HTMLElement} [defaultValue]
  4808. * @returns {HTMLElement | null} dom
  4809. */
  4810. util.option.asElement = function (value, defaultValue) {
  4811. if (typeof value == 'function') {
  4812. value = value();
  4813. }
  4814. return value || defaultValue || null;
  4815. };
  4816. /**
  4817. * Event listener (singleton)
  4818. */
  4819. // TODO: replace usage of the event listener for the EventBus
  4820. var events = {
  4821. 'listeners': [],
  4822. /**
  4823. * Find a single listener by its object
  4824. * @param {Object} object
  4825. * @return {Number} index -1 when not found
  4826. */
  4827. 'indexOf': function (object) {
  4828. var listeners = this.listeners;
  4829. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  4830. var listener = listeners[i];
  4831. if (listener && listener.object == object) {
  4832. return i;
  4833. }
  4834. }
  4835. return -1;
  4836. },
  4837. /**
  4838. * Add an event listener
  4839. * @param {Object} object
  4840. * @param {String} event The name of an event, for example 'select'
  4841. * @param {function} callback The callback method, called when the
  4842. * event takes place
  4843. */
  4844. 'addListener': function (object, event, callback) {
  4845. var index = this.indexOf(object);
  4846. var listener = this.listeners[index];
  4847. if (!listener) {
  4848. listener = {
  4849. 'object': object,
  4850. 'events': {}
  4851. };
  4852. this.listeners.push(listener);
  4853. }
  4854. var callbacks = listener.events[event];
  4855. if (!callbacks) {
  4856. callbacks = [];
  4857. listener.events[event] = callbacks;
  4858. }
  4859. // add the callback if it does not yet exist
  4860. if (callbacks.indexOf(callback) == -1) {
  4861. callbacks.push(callback);
  4862. }
  4863. },
  4864. /**
  4865. * Remove an event listener
  4866. * @param {Object} object
  4867. * @param {String} event The name of an event, for example 'select'
  4868. * @param {function} callback The registered callback method
  4869. */
  4870. 'removeListener': function (object, event, callback) {
  4871. var index = this.indexOf(object);
  4872. var listener = this.listeners[index];
  4873. if (listener) {
  4874. var callbacks = listener.events[event];
  4875. if (callbacks) {
  4876. index = callbacks.indexOf(callback);
  4877. if (index != -1) {
  4878. callbacks.splice(index, 1);
  4879. }
  4880. // remove the array when empty
  4881. if (callbacks.length == 0) {
  4882. delete listener.events[event];
  4883. }
  4884. }
  4885. // count the number of registered events. remove listener when empty
  4886. var count = 0;
  4887. var events = listener.events;
  4888. for (var e in events) {
  4889. if (events.hasOwnProperty(e)) {
  4890. count++;
  4891. }
  4892. }
  4893. if (count == 0) {
  4894. delete this.listeners[index];
  4895. }
  4896. }
  4897. },
  4898. /**
  4899. * Remove all registered event listeners
  4900. */
  4901. 'removeAllListeners': function () {
  4902. this.listeners = [];
  4903. },
  4904. /**
  4905. * Trigger an event. All registered event handlers will be called
  4906. * @param {Object} object
  4907. * @param {String} event
  4908. * @param {Object} properties (optional)
  4909. */
  4910. 'trigger': function (object, event, properties) {
  4911. var index = this.indexOf(object);
  4912. var listener = this.listeners[index];
  4913. if (listener) {
  4914. var callbacks = listener.events[event];
  4915. if (callbacks) {
  4916. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  4917. callbacks[i](properties);
  4918. }
  4919. }
  4920. }
  4921. }
  4922. };
  4923. /**
  4924. * An event bus can be used to emit events, and to subscribe to events
  4925. * @constructor EventBus
  4926. */
  4927. function EventBus() {
  4928. this.subscriptions = [];
  4929. }
  4930. /**
  4931. * Subscribe to an event
  4932. * @param {String | RegExp} event The event can be a regular expression, or
  4933. * a string with wildcards, like 'server.*'.
  4934. * @param {function} callback. Callback are called with three parameters:
  4935. * {String} event, {*} [data], {*} [source]
  4936. * @param {*} [target]
  4937. * @returns {String} id A subscription id
  4938. */
  4939. EventBus.prototype.on = function (event, callback, target) {
  4940. var regexp = (event instanceof RegExp) ?
  4941. event :
  4942. new RegExp(event.replace('*', '\\w+'));
  4943. var subscription = {
  4944. id: util.randomUUID(),
  4945. event: event,
  4946. regexp: regexp,
  4947. callback: (typeof callback === 'function') ? callback : null,
  4948. target: target
  4949. };
  4950. this.subscriptions.push(subscription);
  4951. return subscription.id;
  4952. };
  4953. /**
  4954. * Unsubscribe from an event
  4955. * @param {String | Object} filter Filter for subscriptions to be removed
  4956. * Filter can be a string containing a
  4957. * subscription id, or an object containing
  4958. * one or more of the fields id, event,
  4959. * callback, and target.
  4960. */
  4961. EventBus.prototype.off = function (filter) {
  4962. var i = 0;
  4963. while (i < this.subscriptions.length) {
  4964. var subscription = this.subscriptions[i];
  4965. var match = true;
  4966. if (filter instanceof Object) {
  4967. // filter is an object. All fields must match
  4968. for (var prop in filter) {
  4969. if (filter.hasOwnProperty(prop)) {
  4970. if (filter[prop] !== subscription[prop]) {
  4971. match = false;
  4972. }
  4973. }
  4974. }
  4975. }
  4976. else {
  4977. // filter is a string, filter on id
  4978. match = (subscription.id == filter);
  4979. }
  4980. if (match) {
  4981. this.subscriptions.splice(i, 1);
  4982. }
  4983. else {
  4984. i++;
  4985. }
  4986. }
  4987. };
  4988. /**
  4989. * Emit an event
  4990. * @param {String} event
  4991. * @param {*} [data]
  4992. * @param {*} [source]
  4993. */
  4994. EventBus.prototype.emit = function (event, data, source) {
  4995. for (var i =0; i < this.subscriptions.length; i++) {
  4996. var subscription = this.subscriptions[i];
  4997. if (subscription.regexp.test(event)) {
  4998. if (subscription.callback) {
  4999. subscription.callback(event, data, source);
  5000. }
  5001. }
  5002. }
  5003. };
  5004. /**
  5005. * DataSet
  5006. *
  5007. * Usage:
  5008. * var dataSet = new DataSet({
  5009. * fieldId: '_id',
  5010. * convert: {
  5011. * // ...
  5012. * }
  5013. * });
  5014. *
  5015. * dataSet.add(item);
  5016. * dataSet.add(data);
  5017. * dataSet.update(item);
  5018. * dataSet.update(data);
  5019. * dataSet.remove(id);
  5020. * dataSet.remove(ids);
  5021. * var data = dataSet.get();
  5022. * var data = dataSet.get(id);
  5023. * var data = dataSet.get(ids);
  5024. * var data = dataSet.get(ids, options, data);
  5025. * dataSet.clear();
  5026. *
  5027. * A data set can:
  5028. * - add/remove/update data
  5029. * - gives triggers upon changes in the data
  5030. * - can import/export data in various data formats
  5031. *
  5032. * @param {Object} [options] Available options:
  5033. * {String} fieldId Field name of the id in the
  5034. * items, 'id' by default.
  5035. * {Object.<String, String} convert
  5036. * A map with field names as key,
  5037. * and the field type as value.
  5038. * @constructor DataSet
  5039. */
  5040. // TODO: add a DataSet constructor DataSet(data, options)
  5041. function DataSet (options) {
  5042. this.id = util.randomUUID();
  5043. this.options = options || {};
  5044. this.data = {}; // map with data indexed by id
  5045. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  5046. this.convert = {}; // field types by field name
  5047. this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
  5048. if (this.options.convert) {
  5049. for (var field in this.options.convert) {
  5050. if (this.options.convert.hasOwnProperty(field)) {
  5051. var value = this.options.convert[field];
  5052. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  5053. this.convert[field] = 'Date';
  5054. }
  5055. else {
  5056. this.convert[field] = value;
  5057. }
  5058. }
  5059. }
  5060. }
  5061. // event subscribers
  5062. this.subscribers = {};
  5063. this.internalIds = {}; // internally generated id's
  5064. }
  5065. /**
  5066. * Subscribe to an event, add an event listener
  5067. * @param {String} event Event name. Available events: 'put', 'update',
  5068. * 'remove'
  5069. * @param {function} callback Callback method. Called with three parameters:
  5070. * {String} event
  5071. * {Object | null} params
  5072. * {String | Number} senderId
  5073. */
  5074. DataSet.prototype.subscribe = function (event, callback) {
  5075. var subscribers = this.subscribers[event];
  5076. if (!subscribers) {
  5077. subscribers = [];
  5078. this.subscribers[event] = subscribers;
  5079. }
  5080. subscribers.push({
  5081. callback: callback
  5082. });
  5083. };
  5084. /**
  5085. * Unsubscribe from an event, remove an event listener
  5086. * @param {String} event
  5087. * @param {function} callback
  5088. */
  5089. DataSet.prototype.unsubscribe = function (event, callback) {
  5090. var subscribers = this.subscribers[event];
  5091. if (subscribers) {
  5092. this.subscribers[event] = subscribers.filter(function (listener) {
  5093. return (listener.callback != callback);
  5094. });
  5095. }
  5096. };
  5097. /**
  5098. * Trigger an event
  5099. * @param {String} event
  5100. * @param {Object | null} params
  5101. * @param {String} [senderId] Optional id of the sender.
  5102. * @private
  5103. */
  5104. DataSet.prototype._trigger = function (event, params, senderId) {
  5105. if (event == '*') {
  5106. throw new Error('Cannot trigger event *');
  5107. }
  5108. var subscribers = [];
  5109. if (event in this.subscribers) {
  5110. subscribers = subscribers.concat(this.subscribers[event]);
  5111. }
  5112. if ('*' in this.subscribers) {
  5113. subscribers = subscribers.concat(this.subscribers['*']);
  5114. }
  5115. for (var i = 0; i < subscribers.length; i++) {
  5116. var subscriber = subscribers[i];
  5117. if (subscriber.callback) {
  5118. subscriber.callback(event, params, senderId || null);
  5119. }
  5120. }
  5121. };
  5122. /**
  5123. * Add data.
  5124. * Adding an item will fail when there already is an item with the same id.
  5125. * @param {Object | Array | DataTable} data
  5126. * @param {String} [senderId] Optional sender id
  5127. * @return {Array} addedIds Array with the ids of the added items
  5128. */
  5129. DataSet.prototype.add = function (data, senderId) {
  5130. var addedIds = [],
  5131. id,
  5132. me = this;
  5133. if (data instanceof Array) {
  5134. // Array
  5135. for (var i = 0, len = data.length; i < len; i++) {
  5136. id = me._addItem(data[i]);
  5137. addedIds.push(id);
  5138. }
  5139. }
  5140. else if (util.isDataTable(data)) {
  5141. // Google DataTable
  5142. var columns = this._getColumnNames(data);
  5143. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  5144. var item = {};
  5145. for (var col = 0, cols = columns.length; col < cols; col++) {
  5146. var field = columns[col];
  5147. item[field] = data.getValue(row, col);
  5148. }
  5149. id = me._addItem(item);
  5150. addedIds.push(id);
  5151. }
  5152. }
  5153. else if (data instanceof Object) {
  5154. // Single item
  5155. id = me._addItem(data);
  5156. addedIds.push(id);
  5157. }
  5158. else {
  5159. throw new Error('Unknown dataType');
  5160. }
  5161. if (addedIds.length) {
  5162. this._trigger('add', {items: addedIds}, senderId);
  5163. }
  5164. return addedIds;
  5165. };
  5166. /**
  5167. * Update existing items. When an item does not exist, it will be created
  5168. * @param {Object | Array | DataTable} data
  5169. * @param {String} [senderId] Optional sender id
  5170. * @return {Array} updatedIds The ids of the added or updated items
  5171. */
  5172. DataSet.prototype.update = function (data, senderId) {
  5173. var addedIds = [],
  5174. updatedIds = [],
  5175. me = this,
  5176. fieldId = me.fieldId;
  5177. var addOrUpdate = function (item) {
  5178. var id = item[fieldId];
  5179. if (me.data[id]) {
  5180. // update item
  5181. id = me._updateItem(item);
  5182. updatedIds.push(id);
  5183. }
  5184. else {
  5185. // add new item
  5186. id = me._addItem(item);
  5187. addedIds.push(id);
  5188. }
  5189. };
  5190. if (data instanceof Array) {
  5191. // Array
  5192. for (var i = 0, len = data.length; i < len; i++) {
  5193. addOrUpdate(data[i]);
  5194. }
  5195. }
  5196. else if (util.isDataTable(data)) {
  5197. // Google DataTable
  5198. var columns = this._getColumnNames(data);
  5199. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  5200. var item = {};
  5201. for (var col = 0, cols = columns.length; col < cols; col++) {
  5202. var field = columns[col];
  5203. item[field] = data.getValue(row, col);
  5204. }
  5205. addOrUpdate(item);
  5206. }
  5207. }
  5208. else if (data instanceof Object) {
  5209. // Single item
  5210. addOrUpdate(data);
  5211. }
  5212. else {
  5213. throw new Error('Unknown dataType');
  5214. }
  5215. if (addedIds.length) {
  5216. this._trigger('add', {items: addedIds}, senderId);
  5217. }
  5218. if (updatedIds.length) {
  5219. this._trigger('update', {items: updatedIds}, senderId);
  5220. }
  5221. return addedIds.concat(updatedIds);
  5222. };
  5223. /**
  5224. * Get a data item or multiple items.
  5225. *
  5226. * Usage:
  5227. *
  5228. * get()
  5229. * get(options: Object)
  5230. * get(options: Object, data: Array | DataTable)
  5231. *
  5232. * get(id: Number | String)
  5233. * get(id: Number | String, options: Object)
  5234. * get(id: Number | String, options: Object, data: Array | DataTable)
  5235. *
  5236. * get(ids: Number[] | String[])
  5237. * get(ids: Number[] | String[], options: Object)
  5238. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  5239. *
  5240. * Where:
  5241. *
  5242. * {Number | String} id The id of an item
  5243. * {Number[] | String{}} ids An array with ids of items
  5244. * {Object} options An Object with options. Available options:
  5245. * {String} [type] Type of data to be returned. Can
  5246. * be 'DataTable' or 'Array' (default)
  5247. * {Object.<String, String>} [convert]
  5248. * {String[]} [fields] field names to be returned
  5249. * {function} [filter] filter items
  5250. * {String | function} [order] Order the items by
  5251. * a field name or custom sort function.
  5252. * {Array | DataTable} [data] If provided, items will be appended to this
  5253. * array or table. Required in case of Google
  5254. * DataTable.
  5255. *
  5256. * @throws Error
  5257. */
  5258. DataSet.prototype.get = function (args) {
  5259. var me = this;
  5260. var globalShowInternalIds = this.showInternalIds;
  5261. // parse the arguments
  5262. var id, ids, options, data;
  5263. var firstType = util.getType(arguments[0]);
  5264. if (firstType == 'String' || firstType == 'Number') {
  5265. // get(id [, options] [, data])
  5266. id = arguments[0];
  5267. options = arguments[1];
  5268. data = arguments[2];
  5269. }
  5270. else if (firstType == 'Array') {
  5271. // get(ids [, options] [, data])
  5272. ids = arguments[0];
  5273. options = arguments[1];
  5274. data = arguments[2];
  5275. }
  5276. else {
  5277. // get([, options] [, data])
  5278. options = arguments[0];
  5279. data = arguments[1];
  5280. }
  5281. // determine the return type
  5282. var type;
  5283. if (options && options.type) {
  5284. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  5285. if (data && (type != util.getType(data))) {
  5286. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  5287. 'does not correspond with specified options.type (' + options.type + ')');
  5288. }
  5289. if (type == 'DataTable' && !util.isDataTable(data)) {
  5290. throw new Error('Parameter "data" must be a DataTable ' +
  5291. 'when options.type is "DataTable"');
  5292. }
  5293. }
  5294. else if (data) {
  5295. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  5296. }
  5297. else {
  5298. type = 'Array';
  5299. }
  5300. // we allow the setting of this value for a single get request.
  5301. if (options != undefined) {
  5302. if (options.showInternalIds != undefined) {
  5303. this.showInternalIds = options.showInternalIds;
  5304. }
  5305. }
  5306. // build options
  5307. var convert = options && options.convert || this.options.convert;
  5308. var filter = options && options.filter;
  5309. var items = [], item, itemId, i, len;
  5310. // convert items
  5311. if (id != undefined) {
  5312. // return a single item
  5313. item = me._getItem(id, convert);
  5314. if (filter && !filter(item)) {
  5315. item = null;
  5316. }
  5317. }
  5318. else if (ids != undefined) {
  5319. // return a subset of items
  5320. for (i = 0, len = ids.length; i < len; i++) {
  5321. item = me._getItem(ids[i], convert);
  5322. if (!filter || filter(item)) {
  5323. items.push(item);
  5324. }
  5325. }
  5326. }
  5327. else {
  5328. // return all items
  5329. for (itemId in this.data) {
  5330. if (this.data.hasOwnProperty(itemId)) {
  5331. item = me._getItem(itemId, convert);
  5332. if (!filter || filter(item)) {
  5333. items.push(item);
  5334. }
  5335. }
  5336. }
  5337. }
  5338. // restore the global value of showInternalIds
  5339. this.showInternalIds = globalShowInternalIds;
  5340. // order the results
  5341. if (options && options.order && id == undefined) {
  5342. this._sort(items, options.order);
  5343. }
  5344. // filter fields of the items
  5345. if (options && options.fields) {
  5346. var fields = options.fields;
  5347. if (id != undefined) {
  5348. item = this._filterFields(item, fields);
  5349. }
  5350. else {
  5351. for (i = 0, len = items.length; i < len; i++) {
  5352. items[i] = this._filterFields(items[i], fields);
  5353. }
  5354. }
  5355. }
  5356. // return the results
  5357. if (type == 'DataTable') {
  5358. var columns = this._getColumnNames(data);
  5359. if (id != undefined) {
  5360. // append a single item to the data table
  5361. me._appendRow(data, columns, item);
  5362. }
  5363. else {
  5364. // copy the items to the provided data table
  5365. for (i = 0, len = items.length; i < len; i++) {
  5366. me._appendRow(data, columns, items[i]);
  5367. }
  5368. }
  5369. return data;
  5370. }
  5371. else {
  5372. // return an array
  5373. if (id != undefined) {
  5374. // a single item
  5375. return item;
  5376. }
  5377. else {
  5378. // multiple items
  5379. if (data) {
  5380. // copy the items to the provided array
  5381. for (i = 0, len = items.length; i < len; i++) {
  5382. data.push(items[i]);
  5383. }
  5384. return data;
  5385. }
  5386. else {
  5387. // just return our array
  5388. return items;
  5389. }
  5390. }
  5391. }
  5392. };
  5393. /**
  5394. * Get ids of all items or from a filtered set of items.
  5395. * @param {Object} [options] An Object with options. Available options:
  5396. * {function} [filter] filter items
  5397. * {String | function} [order] Order the items by
  5398. * a field name or custom sort function.
  5399. * @return {Array} ids
  5400. */
  5401. DataSet.prototype.getIds = function (options) {
  5402. var data = this.data,
  5403. filter = options && options.filter,
  5404. order = options && options.order,
  5405. convert = options && options.convert || this.options.convert,
  5406. i,
  5407. len,
  5408. id,
  5409. item,
  5410. items,
  5411. ids = [];
  5412. if (filter) {
  5413. // get filtered items
  5414. if (order) {
  5415. // create ordered list
  5416. items = [];
  5417. for (id in data) {
  5418. if (data.hasOwnProperty(id)) {
  5419. item = this._getItem(id, convert);
  5420. if (filter(item)) {
  5421. items.push(item);
  5422. }
  5423. }
  5424. }
  5425. this._sort(items, order);
  5426. for (i = 0, len = items.length; i < len; i++) {
  5427. ids[i] = items[i][this.fieldId];
  5428. }
  5429. }
  5430. else {
  5431. // create unordered list
  5432. for (id in data) {
  5433. if (data.hasOwnProperty(id)) {
  5434. item = this._getItem(id, convert);
  5435. if (filter(item)) {
  5436. ids.push(item[this.fieldId]);
  5437. }
  5438. }
  5439. }
  5440. }
  5441. }
  5442. else {
  5443. // get all items
  5444. if (order) {
  5445. // create an ordered list
  5446. items = [];
  5447. for (id in data) {
  5448. if (data.hasOwnProperty(id)) {
  5449. items.push(data[id]);
  5450. }
  5451. }
  5452. this._sort(items, order);
  5453. for (i = 0, len = items.length; i < len; i++) {
  5454. ids[i] = items[i][this.fieldId];
  5455. }
  5456. }
  5457. else {
  5458. // create unordered list
  5459. for (id in data) {
  5460. if (data.hasOwnProperty(id)) {
  5461. item = data[id];
  5462. ids.push(item[this.fieldId]);
  5463. }
  5464. }
  5465. }
  5466. }
  5467. return ids;
  5468. };
  5469. /**
  5470. * Execute a callback function for every item in the dataset.
  5471. * The order of the items is not determined.
  5472. * @param {function} callback
  5473. * @param {Object} [options] Available options:
  5474. * {Object.<String, String>} [convert]
  5475. * {String[]} [fields] filter fields
  5476. * {function} [filter] filter items
  5477. * {String | function} [order] Order the items by
  5478. * a field name or custom sort function.
  5479. */
  5480. DataSet.prototype.forEach = function (callback, options) {
  5481. var filter = options && options.filter,
  5482. convert = options && options.convert || this.options.convert,
  5483. data = this.data,
  5484. item,
  5485. id;
  5486. if (options && options.order) {
  5487. // execute forEach on ordered list
  5488. var items = this.get(options);
  5489. for (var i = 0, len = items.length; i < len; i++) {
  5490. item = items[i];
  5491. id = item[this.fieldId];
  5492. callback(item, id);
  5493. }
  5494. }
  5495. else {
  5496. // unordered
  5497. for (id in data) {
  5498. if (data.hasOwnProperty(id)) {
  5499. item = this._getItem(id, convert);
  5500. if (!filter || filter(item)) {
  5501. callback(item, id);
  5502. }
  5503. }
  5504. }
  5505. }
  5506. };
  5507. /**
  5508. * Map every item in the dataset.
  5509. * @param {function} callback
  5510. * @param {Object} [options] Available options:
  5511. * {Object.<String, String>} [convert]
  5512. * {String[]} [fields] filter fields
  5513. * {function} [filter] filter items
  5514. * {String | function} [order] Order the items by
  5515. * a field name or custom sort function.
  5516. * @return {Object[]} mappedItems
  5517. */
  5518. DataSet.prototype.map = function (callback, options) {
  5519. var filter = options && options.filter,
  5520. convert = options && options.convert || this.options.convert,
  5521. mappedItems = [],
  5522. data = this.data,
  5523. item;
  5524. // convert and filter items
  5525. for (var id in data) {
  5526. if (data.hasOwnProperty(id)) {
  5527. item = this._getItem(id, convert);
  5528. if (!filter || filter(item)) {
  5529. mappedItems.push(callback(item, id));
  5530. }
  5531. }
  5532. }
  5533. // order items
  5534. if (options && options.order) {
  5535. this._sort(mappedItems, options.order);
  5536. }
  5537. return mappedItems;
  5538. };
  5539. /**
  5540. * Filter the fields of an item
  5541. * @param {Object} item
  5542. * @param {String[]} fields Field names
  5543. * @return {Object} filteredItem
  5544. * @private
  5545. */
  5546. DataSet.prototype._filterFields = function (item, fields) {
  5547. var filteredItem = {};
  5548. for (var field in item) {
  5549. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  5550. filteredItem[field] = item[field];
  5551. }
  5552. }
  5553. return filteredItem;
  5554. };
  5555. /**
  5556. * Sort the provided array with items
  5557. * @param {Object[]} items
  5558. * @param {String | function} order A field name or custom sort function.
  5559. * @private
  5560. */
  5561. DataSet.prototype._sort = function (items, order) {
  5562. if (util.isString(order)) {
  5563. // order by provided field name
  5564. var name = order; // field name
  5565. items.sort(function (a, b) {
  5566. var av = a[name];
  5567. var bv = b[name];
  5568. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  5569. });
  5570. }
  5571. else if (typeof order === 'function') {
  5572. // order by sort function
  5573. items.sort(order);
  5574. }
  5575. // TODO: extend order by an Object {field:String, direction:String}
  5576. // where direction can be 'asc' or 'desc'
  5577. else {
  5578. throw new TypeError('Order must be a function or a string');
  5579. }
  5580. };
  5581. /**
  5582. * Remove an object by pointer or by id
  5583. * @param {String | Number | Object | Array} id Object or id, or an array with
  5584. * objects or ids to be removed
  5585. * @param {String} [senderId] Optional sender id
  5586. * @return {Array} removedIds
  5587. */
  5588. DataSet.prototype.remove = function (id, senderId) {
  5589. var removedIds = [],
  5590. i, len, removedId;
  5591. if (id instanceof Array) {
  5592. for (i = 0, len = id.length; i < len; i++) {
  5593. removedId = this._remove(id[i]);
  5594. if (removedId != null) {
  5595. removedIds.push(removedId);
  5596. }
  5597. }
  5598. }
  5599. else {
  5600. removedId = this._remove(id);
  5601. if (removedId != null) {
  5602. removedIds.push(removedId);
  5603. }
  5604. }
  5605. if (removedIds.length) {
  5606. this._trigger('remove', {items: removedIds}, senderId);
  5607. }
  5608. return removedIds;
  5609. };
  5610. /**
  5611. * Remove an item by its id
  5612. * @param {Number | String | Object} id id or item
  5613. * @returns {Number | String | null} id
  5614. * @private
  5615. */
  5616. DataSet.prototype._remove = function (id) {
  5617. if (util.isNumber(id) || util.isString(id)) {
  5618. if (this.data[id]) {
  5619. delete this.data[id];
  5620. delete this.internalIds[id];
  5621. return id;
  5622. }
  5623. }
  5624. else if (id instanceof Object) {
  5625. var itemId = id[this.fieldId];
  5626. if (itemId && this.data[itemId]) {
  5627. delete this.data[itemId];
  5628. delete this.internalIds[itemId];
  5629. return itemId;
  5630. }
  5631. }
  5632. return null;
  5633. };
  5634. /**
  5635. * Clear the data
  5636. * @param {String} [senderId] Optional sender id
  5637. * @return {Array} removedIds The ids of all removed items
  5638. */
  5639. DataSet.prototype.clear = function (senderId) {
  5640. var ids = Object.keys(this.data);
  5641. this.data = {};
  5642. this.internalIds = {};
  5643. this._trigger('remove', {items: ids}, senderId);
  5644. return ids;
  5645. };
  5646. /**
  5647. * Find the item with maximum value of a specified field
  5648. * @param {String} field
  5649. * @return {Object | null} item Item containing max value, or null if no items
  5650. */
  5651. DataSet.prototype.max = function (field) {
  5652. var data = this.data,
  5653. max = null,
  5654. maxField = null;
  5655. for (var id in data) {
  5656. if (data.hasOwnProperty(id)) {
  5657. var item = data[id];
  5658. var itemField = item[field];
  5659. if (itemField != null && (!max || itemField > maxField)) {
  5660. max = item;
  5661. maxField = itemField;
  5662. }
  5663. }
  5664. }
  5665. return max;
  5666. };
  5667. /**
  5668. * Find the item with minimum value of a specified field
  5669. * @param {String} field
  5670. * @return {Object | null} item Item containing max value, or null if no items
  5671. */
  5672. DataSet.prototype.min = function (field) {
  5673. var data = this.data,
  5674. min = null,
  5675. minField = null;
  5676. for (var id in data) {
  5677. if (data.hasOwnProperty(id)) {
  5678. var item = data[id];
  5679. var itemField = item[field];
  5680. if (itemField != null && (!min || itemField < minField)) {
  5681. min = item;
  5682. minField = itemField;
  5683. }
  5684. }
  5685. }
  5686. return min;
  5687. };
  5688. /**
  5689. * Find all distinct values of a specified field
  5690. * @param {String} field
  5691. * @return {Array} values Array containing all distinct values. If the data
  5692. * items do not contain the specified field, an array
  5693. * containing a single value undefined is returned.
  5694. * The returned array is unordered.
  5695. */
  5696. DataSet.prototype.distinct = function (field) {
  5697. var data = this.data,
  5698. values = [],
  5699. fieldType = this.options.convert[field],
  5700. count = 0;
  5701. for (var prop in data) {
  5702. if (data.hasOwnProperty(prop)) {
  5703. var item = data[prop];
  5704. var value = util.convert(item[field], fieldType);
  5705. var exists = false;
  5706. for (var i = 0; i < count; i++) {
  5707. if (values[i] == value) {
  5708. exists = true;
  5709. break;
  5710. }
  5711. }
  5712. if (!exists) {
  5713. values[count] = value;
  5714. count++;
  5715. }
  5716. }
  5717. }
  5718. return values;
  5719. };
  5720. /**
  5721. * Add a single item. Will fail when an item with the same id already exists.
  5722. * @param {Object} item
  5723. * @return {String} id
  5724. * @private
  5725. */
  5726. DataSet.prototype._addItem = function (item) {
  5727. var id = item[this.fieldId];
  5728. if (id != undefined) {
  5729. // check whether this id is already taken
  5730. if (this.data[id]) {
  5731. // item already exists
  5732. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  5733. }
  5734. }
  5735. else {
  5736. // generate an id
  5737. id = util.randomUUID();
  5738. item[this.fieldId] = id;
  5739. this.internalIds[id] = item;
  5740. }
  5741. var d = {};
  5742. for (var field in item) {
  5743. if (item.hasOwnProperty(field)) {
  5744. var fieldType = this.convert[field]; // type may be undefined
  5745. d[field] = util.convert(item[field], fieldType);
  5746. }
  5747. }
  5748. this.data[id] = d;
  5749. return id;
  5750. };
  5751. /**
  5752. * Get an item. Fields can be converted to a specific type
  5753. * @param {String} id
  5754. * @param {Object.<String, String>} [convert] field types to convert
  5755. * @return {Object | null} item
  5756. * @private
  5757. */
  5758. DataSet.prototype._getItem = function (id, convert) {
  5759. var field, value;
  5760. // get the item from the dataset
  5761. var raw = this.data[id];
  5762. if (!raw) {
  5763. return null;
  5764. }
  5765. // convert the items field types
  5766. var converted = {},
  5767. fieldId = this.fieldId,
  5768. internalIds = this.internalIds;
  5769. if (convert) {
  5770. for (field in raw) {
  5771. if (raw.hasOwnProperty(field)) {
  5772. value = raw[field];
  5773. // output all fields, except internal ids
  5774. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  5775. converted[field] = util.convert(value, convert[field]);
  5776. }
  5777. }
  5778. }
  5779. }
  5780. else {
  5781. // no field types specified, no converting needed
  5782. for (field in raw) {
  5783. if (raw.hasOwnProperty(field)) {
  5784. value = raw[field];
  5785. // output all fields, except internal ids
  5786. if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
  5787. converted[field] = value;
  5788. }
  5789. }
  5790. }
  5791. }
  5792. return converted;
  5793. };
  5794. /**
  5795. * Update a single item: merge with existing item.
  5796. * Will fail when the item has no id, or when there does not exist an item
  5797. * with the same id.
  5798. * @param {Object} item
  5799. * @return {String} id
  5800. * @private
  5801. */
  5802. DataSet.prototype._updateItem = function (item) {
  5803. var id = item[this.fieldId];
  5804. if (id == undefined) {
  5805. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  5806. }
  5807. var d = this.data[id];
  5808. if (!d) {
  5809. // item doesn't exist
  5810. throw new Error('Cannot update item: no item with id ' + id + ' found');
  5811. }
  5812. // merge with current item
  5813. for (var field in item) {
  5814. if (item.hasOwnProperty(field)) {
  5815. var fieldType = this.convert[field]; // type may be undefined
  5816. d[field] = util.convert(item[field], fieldType);
  5817. }
  5818. }
  5819. return id;
  5820. };
  5821. /**
  5822. * check if an id is an internal or external id
  5823. * @param id
  5824. * @returns {boolean}
  5825. * @private
  5826. */
  5827. DataSet.prototype.isInternalId = function(id) {
  5828. return (id in this.internalIds);
  5829. };
  5830. /**
  5831. * Get an array with the column names of a Google DataTable
  5832. * @param {DataTable} dataTable
  5833. * @return {String[]} columnNames
  5834. * @private
  5835. */
  5836. DataSet.prototype._getColumnNames = function (dataTable) {
  5837. var columns = [];
  5838. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  5839. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  5840. }
  5841. return columns;
  5842. };
  5843. /**
  5844. * Append an item as a row to the dataTable
  5845. * @param dataTable
  5846. * @param columns
  5847. * @param item
  5848. * @private
  5849. */
  5850. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  5851. var row = dataTable.addRow();
  5852. for (var col = 0, cols = columns.length; col < cols; col++) {
  5853. var field = columns[col];
  5854. dataTable.setValue(row, col, item[field]);
  5855. }
  5856. };
  5857. /**
  5858. * DataView
  5859. *
  5860. * a dataview offers a filtered view on a dataset or an other dataview.
  5861. *
  5862. * @param {DataSet | DataView} data
  5863. * @param {Object} [options] Available options: see method get
  5864. *
  5865. * @constructor DataView
  5866. */
  5867. function DataView (data, options) {
  5868. this.id = util.randomUUID();
  5869. this.data = null;
  5870. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  5871. this.options = options || {};
  5872. this.fieldId = 'id'; // name of the field containing id
  5873. this.subscribers = {}; // event subscribers
  5874. var me = this;
  5875. this.listener = function () {
  5876. me._onEvent.apply(me, arguments);
  5877. };
  5878. this.setData(data);
  5879. }
  5880. // TODO: implement a function .config() to dynamically update things like configured filter
  5881. // and trigger changes accordingly
  5882. /**
  5883. * Set a data source for the view
  5884. * @param {DataSet | DataView} data
  5885. */
  5886. DataView.prototype.setData = function (data) {
  5887. var ids, dataItems, i, len;
  5888. if (this.data) {
  5889. // unsubscribe from current dataset
  5890. if (this.data.unsubscribe) {
  5891. this.data.unsubscribe('*', this.listener);
  5892. }
  5893. // trigger a remove of all items in memory
  5894. ids = [];
  5895. for (var id in this.ids) {
  5896. if (this.ids.hasOwnProperty(id)) {
  5897. ids.push(id);
  5898. }
  5899. }
  5900. this.ids = {};
  5901. this._trigger('remove', {items: ids});
  5902. }
  5903. this.data = data;
  5904. if (this.data) {
  5905. // update fieldId
  5906. this.fieldId = this.options.fieldId ||
  5907. (this.data && this.data.options && this.data.options.fieldId) ||
  5908. 'id';
  5909. // trigger an add of all added items
  5910. ids = this.data.getIds({filter: this.options && this.options.filter});
  5911. for (i = 0, len = ids.length; i < len; i++) {
  5912. id = ids[i];
  5913. this.ids[id] = true;
  5914. }
  5915. this._trigger('add', {items: ids});
  5916. // subscribe to new dataset
  5917. if (this.data.subscribe) {
  5918. this.data.subscribe('*', this.listener);
  5919. }
  5920. }
  5921. };
  5922. /**
  5923. * Get data from the data view
  5924. *
  5925. * Usage:
  5926. *
  5927. * get()
  5928. * get(options: Object)
  5929. * get(options: Object, data: Array | DataTable)
  5930. *
  5931. * get(id: Number)
  5932. * get(id: Number, options: Object)
  5933. * get(id: Number, options: Object, data: Array | DataTable)
  5934. *
  5935. * get(ids: Number[])
  5936. * get(ids: Number[], options: Object)
  5937. * get(ids: Number[], options: Object, data: Array | DataTable)
  5938. *
  5939. * Where:
  5940. *
  5941. * {Number | String} id The id of an item
  5942. * {Number[] | String{}} ids An array with ids of items
  5943. * {Object} options An Object with options. Available options:
  5944. * {String} [type] Type of data to be returned. Can
  5945. * be 'DataTable' or 'Array' (default)
  5946. * {Object.<String, String>} [convert]
  5947. * {String[]} [fields] field names to be returned
  5948. * {function} [filter] filter items
  5949. * {String | function} [order] Order the items by
  5950. * a field name or custom sort function.
  5951. * {Array | DataTable} [data] If provided, items will be appended to this
  5952. * array or table. Required in case of Google
  5953. * DataTable.
  5954. * @param args
  5955. */
  5956. DataView.prototype.get = function (args) {
  5957. var me = this;
  5958. // parse the arguments
  5959. var ids, options, data;
  5960. var firstType = util.getType(arguments[0]);
  5961. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  5962. // get(id(s) [, options] [, data])
  5963. ids = arguments[0]; // can be a single id or an array with ids
  5964. options = arguments[1];
  5965. data = arguments[2];
  5966. }
  5967. else {
  5968. // get([, options] [, data])
  5969. options = arguments[0];
  5970. data = arguments[1];
  5971. }
  5972. // extend the options with the default options and provided options
  5973. var viewOptions = util.extend({}, this.options, options);
  5974. // create a combined filter method when needed
  5975. if (this.options.filter && options && options.filter) {
  5976. viewOptions.filter = function (item) {
  5977. return me.options.filter(item) && options.filter(item);
  5978. }
  5979. }
  5980. // build up the call to the linked data set
  5981. var getArguments = [];
  5982. if (ids != undefined) {
  5983. getArguments.push(ids);
  5984. }
  5985. getArguments.push(viewOptions);
  5986. getArguments.push(data);
  5987. return this.data && this.data.get.apply(this.data, getArguments);
  5988. };
  5989. /**
  5990. * Get ids of all items or from a filtered set of items.
  5991. * @param {Object} [options] An Object with options. Available options:
  5992. * {function} [filter] filter items
  5993. * {String | function} [order] Order the items by
  5994. * a field name or custom sort function.
  5995. * @return {Array} ids
  5996. */
  5997. DataView.prototype.getIds = function (options) {
  5998. var ids;
  5999. if (this.data) {
  6000. var defaultFilter = this.options.filter;
  6001. var filter;
  6002. if (options && options.filter) {
  6003. if (defaultFilter) {
  6004. filter = function (item) {
  6005. return defaultFilter(item) && options.filter(item);
  6006. }
  6007. }
  6008. else {
  6009. filter = options.filter;
  6010. }
  6011. }
  6012. else {
  6013. filter = defaultFilter;
  6014. }
  6015. ids = this.data.getIds({
  6016. filter: filter,
  6017. order: options && options.order
  6018. });
  6019. }
  6020. else {
  6021. ids = [];
  6022. }
  6023. return ids;
  6024. };
  6025. /**
  6026. * Event listener. Will propagate all events from the connected data set to
  6027. * the subscribers of the DataView, but will filter the items and only trigger
  6028. * when there are changes in the filtered data set.
  6029. * @param {String} event
  6030. * @param {Object | null} params
  6031. * @param {String} senderId
  6032. * @private
  6033. */
  6034. DataView.prototype._onEvent = function (event, params, senderId) {
  6035. var i, len, id, item,
  6036. ids = params && params.items,
  6037. data = this.data,
  6038. added = [],
  6039. updated = [],
  6040. removed = [];
  6041. if (ids && data) {
  6042. switch (event) {
  6043. case 'add':
  6044. // filter the ids of the added items
  6045. for (i = 0, len = ids.length; i < len; i++) {
  6046. id = ids[i];
  6047. item = this.get(id);
  6048. if (item) {
  6049. this.ids[id] = true;
  6050. added.push(id);
  6051. }
  6052. }
  6053. break;
  6054. case 'update':
  6055. // determine the event from the views viewpoint: an updated
  6056. // item can be added, updated, or removed from this view.
  6057. for (i = 0, len = ids.length; i < len; i++) {
  6058. id = ids[i];
  6059. item = this.get(id);
  6060. if (item) {
  6061. if (this.ids[id]) {
  6062. updated.push(id);
  6063. }
  6064. else {
  6065. this.ids[id] = true;
  6066. added.push(id);
  6067. }
  6068. }
  6069. else {
  6070. if (this.ids[id]) {
  6071. delete this.ids[id];
  6072. removed.push(id);
  6073. }
  6074. else {
  6075. // nothing interesting for me :-(
  6076. }
  6077. }
  6078. }
  6079. break;
  6080. case 'remove':
  6081. // filter the ids of the removed items
  6082. for (i = 0, len = ids.length; i < len; i++) {
  6083. id = ids[i];
  6084. if (this.ids[id]) {
  6085. delete this.ids[id];
  6086. removed.push(id);
  6087. }
  6088. }
  6089. break;
  6090. }
  6091. if (added.length) {
  6092. this._trigger('add', {items: added}, senderId);
  6093. }
  6094. if (updated.length) {
  6095. this._trigger('update', {items: updated}, senderId);
  6096. }
  6097. if (removed.length) {
  6098. this._trigger('remove', {items: removed}, senderId);
  6099. }
  6100. }
  6101. };
  6102. // copy subscription functionality from DataSet
  6103. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  6104. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  6105. DataView.prototype._trigger = DataSet.prototype._trigger;
  6106. /**
  6107. * @constructor TimeStep
  6108. * The class TimeStep is an iterator for dates. You provide a start date and an
  6109. * end date. The class itself determines the best scale (step size) based on the
  6110. * provided start Date, end Date, and minimumStep.
  6111. *
  6112. * If minimumStep is provided, the step size is chosen as close as possible
  6113. * to the minimumStep but larger than minimumStep. If minimumStep is not
  6114. * provided, the scale is set to 1 DAY.
  6115. * The minimumStep should correspond with the onscreen size of about 6 characters
  6116. *
  6117. * Alternatively, you can set a scale by hand.
  6118. * After creation, you can initialize the class by executing first(). Then you
  6119. * can iterate from the start date to the end date via next(). You can check if
  6120. * the end date is reached with the function hasNext(). After each step, you can
  6121. * retrieve the current date via getCurrent().
  6122. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  6123. * days, to years.
  6124. *
  6125. * Version: 1.2
  6126. *
  6127. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  6128. * or new Date(2010, 9, 21, 23, 45, 00)
  6129. * @param {Date} [end] The end date
  6130. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  6131. */
  6132. TimeStep = function(start, end, minimumStep) {
  6133. // variables
  6134. this.current = new Date();
  6135. this._start = new Date();
  6136. this._end = new Date();
  6137. this.autoScale = true;
  6138. this.scale = TimeStep.SCALE.DAY;
  6139. this.step = 1;
  6140. // initialize the range
  6141. this.setRange(start, end, minimumStep);
  6142. };
  6143. /// enum scale
  6144. TimeStep.SCALE = {
  6145. MILLISECOND: 1,
  6146. SECOND: 2,
  6147. MINUTE: 3,
  6148. HOUR: 4,
  6149. DAY: 5,
  6150. WEEKDAY: 6,
  6151. MONTH: 7,
  6152. YEAR: 8
  6153. };
  6154. /**
  6155. * Set a new range
  6156. * If minimumStep is provided, the step size is chosen as close as possible
  6157. * to the minimumStep but larger than minimumStep. If minimumStep is not
  6158. * provided, the scale is set to 1 DAY.
  6159. * The minimumStep should correspond with the onscreen size of about 6 characters
  6160. * @param {Date} [start] The start date and time.
  6161. * @param {Date} [end] The end date and time.
  6162. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  6163. */
  6164. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  6165. if (!(start instanceof Date) || !(end instanceof Date)) {
  6166. throw "No legal start or end date in method setRange";
  6167. }
  6168. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  6169. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  6170. if (this.autoScale) {
  6171. this.setMinimumStep(minimumStep);
  6172. }
  6173. };
  6174. /**
  6175. * Set the range iterator to the start date.
  6176. */
  6177. TimeStep.prototype.first = function() {
  6178. this.current = new Date(this._start.valueOf());
  6179. this.roundToMinor();
  6180. };
  6181. /**
  6182. * Round the current date to the first minor date value
  6183. * This must be executed once when the current date is set to start Date
  6184. */
  6185. TimeStep.prototype.roundToMinor = function() {
  6186. // round to floor
  6187. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  6188. //noinspection FallthroughInSwitchStatementJS
  6189. switch (this.scale) {
  6190. case TimeStep.SCALE.YEAR:
  6191. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  6192. this.current.setMonth(0);
  6193. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  6194. case TimeStep.SCALE.DAY: // intentional fall through
  6195. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  6196. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  6197. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  6198. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  6199. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  6200. }
  6201. if (this.step != 1) {
  6202. // round down to the first minor value that is a multiple of the current step size
  6203. switch (this.scale) {
  6204. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  6205. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  6206. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  6207. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  6208. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  6209. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  6210. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  6211. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  6212. default: break;
  6213. }
  6214. }
  6215. };
  6216. /**
  6217. * Check if the there is a next step
  6218. * @return {boolean} true if the current date has not passed the end date
  6219. */
  6220. TimeStep.prototype.hasNext = function () {
  6221. return (this.current.valueOf() <= this._end.valueOf());
  6222. };
  6223. /**
  6224. * Do the next step
  6225. */
  6226. TimeStep.prototype.next = function() {
  6227. var prev = this.current.valueOf();
  6228. // Two cases, needed to prevent issues with switching daylight savings
  6229. // (end of March and end of October)
  6230. if (this.current.getMonth() < 6) {
  6231. switch (this.scale) {
  6232. case TimeStep.SCALE.MILLISECOND:
  6233. this.current = new Date(this.current.valueOf() + this.step); break;
  6234. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  6235. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  6236. case TimeStep.SCALE.HOUR:
  6237. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  6238. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  6239. var h = this.current.getHours();
  6240. this.current.setHours(h - (h % this.step));
  6241. break;
  6242. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  6243. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  6244. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  6245. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  6246. default: break;
  6247. }
  6248. }
  6249. else {
  6250. switch (this.scale) {
  6251. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  6252. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  6253. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  6254. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  6255. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  6256. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  6257. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  6258. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  6259. default: break;
  6260. }
  6261. }
  6262. if (this.step != 1) {
  6263. // round down to the correct major value
  6264. switch (this.scale) {
  6265. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  6266. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  6267. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  6268. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  6269. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  6270. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  6271. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  6272. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  6273. default: break;
  6274. }
  6275. }
  6276. // safety mechanism: if current time is still unchanged, move to the end
  6277. if (this.current.valueOf() == prev) {
  6278. this.current = new Date(this._end.valueOf());
  6279. }
  6280. };
  6281. /**
  6282. * Get the current datetime
  6283. * @return {Date} current The current date
  6284. */
  6285. TimeStep.prototype.getCurrent = function() {
  6286. return this.current;
  6287. };
  6288. /**
  6289. * Set a custom scale. Autoscaling will be disabled.
  6290. * For example setScale(SCALE.MINUTES, 5) will result
  6291. * in minor steps of 5 minutes, and major steps of an hour.
  6292. *
  6293. * @param {TimeStep.SCALE} newScale
  6294. * A scale. Choose from SCALE.MILLISECOND,
  6295. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  6296. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  6297. * SCALE.YEAR.
  6298. * @param {Number} newStep A step size, by default 1. Choose for
  6299. * example 1, 2, 5, or 10.
  6300. */
  6301. TimeStep.prototype.setScale = function(newScale, newStep) {
  6302. this.scale = newScale;
  6303. if (newStep > 0) {
  6304. this.step = newStep;
  6305. }
  6306. this.autoScale = false;
  6307. };
  6308. /**
  6309. * Enable or disable autoscaling
  6310. * @param {boolean} enable If true, autoascaling is set true
  6311. */
  6312. TimeStep.prototype.setAutoScale = function (enable) {
  6313. this.autoScale = enable;
  6314. };
  6315. /**
  6316. * Automatically determine the scale that bests fits the provided minimum step
  6317. * @param {Number} [minimumStep] The minimum step size in milliseconds
  6318. */
  6319. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  6320. if (minimumStep == undefined) {
  6321. return;
  6322. }
  6323. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  6324. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  6325. var stepDay = (1000 * 60 * 60 * 24);
  6326. var stepHour = (1000 * 60 * 60);
  6327. var stepMinute = (1000 * 60);
  6328. var stepSecond = (1000);
  6329. var stepMillisecond= (1);
  6330. // find the smallest step that is larger than the provided minimumStep
  6331. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  6332. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  6333. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  6334. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  6335. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  6336. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  6337. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  6338. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  6339. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  6340. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  6341. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  6342. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  6343. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  6344. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  6345. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  6346. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  6347. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  6348. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  6349. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  6350. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  6351. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  6352. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  6353. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  6354. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  6355. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  6356. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  6357. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  6358. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  6359. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  6360. };
  6361. /**
  6362. * Snap a date to a rounded value. The snap intervals are dependent on the
  6363. * current scale and step.
  6364. * @param {Date} date the date to be snapped
  6365. */
  6366. TimeStep.prototype.snap = function(date) {
  6367. if (this.scale == TimeStep.SCALE.YEAR) {
  6368. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  6369. date.setFullYear(Math.round(year / this.step) * this.step);
  6370. date.setMonth(0);
  6371. date.setDate(0);
  6372. date.setHours(0);
  6373. date.setMinutes(0);
  6374. date.setSeconds(0);
  6375. date.setMilliseconds(0);
  6376. }
  6377. else if (this.scale == TimeStep.SCALE.MONTH) {
  6378. if (date.getDate() > 15) {
  6379. date.setDate(1);
  6380. date.setMonth(date.getMonth() + 1);
  6381. // important: first set Date to 1, after that change the month.
  6382. }
  6383. else {
  6384. date.setDate(1);
  6385. }
  6386. date.setHours(0);
  6387. date.setMinutes(0);
  6388. date.setSeconds(0);
  6389. date.setMilliseconds(0);
  6390. }
  6391. else if (this.scale == TimeStep.SCALE.DAY ||
  6392. this.scale == TimeStep.SCALE.WEEKDAY) {
  6393. //noinspection FallthroughInSwitchStatementJS
  6394. switch (this.step) {
  6395. case 5:
  6396. case 2:
  6397. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  6398. default:
  6399. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  6400. }
  6401. date.setMinutes(0);
  6402. date.setSeconds(0);
  6403. date.setMilliseconds(0);
  6404. }
  6405. else if (this.scale == TimeStep.SCALE.HOUR) {
  6406. switch (this.step) {
  6407. case 4:
  6408. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  6409. default:
  6410. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  6411. }
  6412. date.setSeconds(0);
  6413. date.setMilliseconds(0);
  6414. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  6415. //noinspection FallthroughInSwitchStatementJS
  6416. switch (this.step) {
  6417. case 15:
  6418. case 10:
  6419. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  6420. date.setSeconds(0);
  6421. break;
  6422. case 5:
  6423. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  6424. default:
  6425. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  6426. }
  6427. date.setMilliseconds(0);
  6428. }
  6429. else if (this.scale == TimeStep.SCALE.SECOND) {
  6430. //noinspection FallthroughInSwitchStatementJS
  6431. switch (this.step) {
  6432. case 15:
  6433. case 10:
  6434. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  6435. date.setMilliseconds(0);
  6436. break;
  6437. case 5:
  6438. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  6439. default:
  6440. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  6441. }
  6442. }
  6443. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  6444. var step = this.step > 5 ? this.step / 2 : 1;
  6445. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  6446. }
  6447. };
  6448. /**
  6449. * Check if the current value is a major value (for example when the step
  6450. * is DAY, a major value is each first day of the MONTH)
  6451. * @return {boolean} true if current date is major, else false.
  6452. */
  6453. TimeStep.prototype.isMajor = function() {
  6454. switch (this.scale) {
  6455. case TimeStep.SCALE.MILLISECOND:
  6456. return (this.current.getMilliseconds() == 0);
  6457. case TimeStep.SCALE.SECOND:
  6458. return (this.current.getSeconds() == 0);
  6459. case TimeStep.SCALE.MINUTE:
  6460. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  6461. // Note: this is no bug. Major label is equal for both minute and hour scale
  6462. case TimeStep.SCALE.HOUR:
  6463. return (this.current.getHours() == 0);
  6464. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  6465. case TimeStep.SCALE.DAY:
  6466. return (this.current.getDate() == 1);
  6467. case TimeStep.SCALE.MONTH:
  6468. return (this.current.getMonth() == 0);
  6469. case TimeStep.SCALE.YEAR:
  6470. return false;
  6471. default:
  6472. return false;
  6473. }
  6474. };
  6475. /**
  6476. * Returns formatted text for the minor axislabel, depending on the current
  6477. * date and the scale. For example when scale is MINUTE, the current time is
  6478. * formatted as "hh:mm".
  6479. * @param {Date} [date] custom date. if not provided, current date is taken
  6480. */
  6481. TimeStep.prototype.getLabelMinor = function(date) {
  6482. if (date == undefined) {
  6483. date = this.current;
  6484. }
  6485. switch (this.scale) {
  6486. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  6487. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  6488. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  6489. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  6490. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  6491. case TimeStep.SCALE.DAY: return moment(date).format('D');
  6492. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  6493. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  6494. default: return '';
  6495. }
  6496. };
  6497. /**
  6498. * Returns formatted text for the major axis label, depending on the current
  6499. * date and the scale. For example when scale is MINUTE, the major scale is
  6500. * hours, and the hour will be formatted as "hh".
  6501. * @param {Date} [date] custom date. if not provided, current date is taken
  6502. */
  6503. TimeStep.prototype.getLabelMajor = function(date) {
  6504. if (date == undefined) {
  6505. date = this.current;
  6506. }
  6507. //noinspection FallthroughInSwitchStatementJS
  6508. switch (this.scale) {
  6509. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  6510. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  6511. case TimeStep.SCALE.MINUTE:
  6512. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  6513. case TimeStep.SCALE.WEEKDAY:
  6514. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  6515. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  6516. case TimeStep.SCALE.YEAR: return '';
  6517. default: return '';
  6518. }
  6519. };
  6520. /**
  6521. * @constructor Stack
  6522. * Stacks items on top of each other.
  6523. * @param {ItemSet} parent
  6524. * @param {Object} [options]
  6525. */
  6526. function Stack (parent, options) {
  6527. this.parent = parent;
  6528. this.options = options || {};
  6529. this.defaultOptions = {
  6530. order: function (a, b) {
  6531. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  6532. // Order: ranges over non-ranges, ranged ordered by width, and
  6533. // lastly ordered by start.
  6534. if (a instanceof ItemRange) {
  6535. if (b instanceof ItemRange) {
  6536. var aInt = (a.data.end - a.data.start);
  6537. var bInt = (b.data.end - b.data.start);
  6538. return (aInt - bInt) || (a.data.start - b.data.start);
  6539. }
  6540. else {
  6541. return -1;
  6542. }
  6543. }
  6544. else {
  6545. if (b instanceof ItemRange) {
  6546. return 1;
  6547. }
  6548. else {
  6549. return (a.data.start - b.data.start);
  6550. }
  6551. }
  6552. },
  6553. margin: {
  6554. item: 10
  6555. }
  6556. };
  6557. this.ordered = []; // ordered items
  6558. }
  6559. /**
  6560. * Set options for the stack
  6561. * @param {Object} options Available options:
  6562. * {ItemSet} parent
  6563. * {Number} margin
  6564. * {function} order Stacking order
  6565. */
  6566. Stack.prototype.setOptions = function setOptions (options) {
  6567. util.extend(this.options, options);
  6568. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  6569. };
  6570. /**
  6571. * Stack the items such that they don't overlap. The items will have a minimal
  6572. * distance equal to options.margin.item.
  6573. */
  6574. Stack.prototype.update = function update() {
  6575. this._order();
  6576. this._stack();
  6577. };
  6578. /**
  6579. * Order the items. The items are ordered by width first, and by left position
  6580. * second.
  6581. * If a custom order function has been provided via the options, then this will
  6582. * be used.
  6583. * @private
  6584. */
  6585. Stack.prototype._order = function _order () {
  6586. var items = this.parent.items;
  6587. if (!items) {
  6588. throw new Error('Cannot stack items: parent does not contain items');
  6589. }
  6590. // TODO: store the sorted items, to have less work later on
  6591. var ordered = [];
  6592. var index = 0;
  6593. // items is a map (no array)
  6594. util.forEach(items, function (item) {
  6595. if (item.visible) {
  6596. ordered[index] = item;
  6597. index++;
  6598. }
  6599. });
  6600. //if a customer stack order function exists, use it.
  6601. var order = this.options.order || this.defaultOptions.order;
  6602. if (!(typeof order === 'function')) {
  6603. throw new Error('Option order must be a function');
  6604. }
  6605. ordered.sort(order);
  6606. this.ordered = ordered;
  6607. };
  6608. /**
  6609. * Adjust vertical positions of the events such that they don't overlap each
  6610. * other.
  6611. * @private
  6612. */
  6613. Stack.prototype._stack = function _stack () {
  6614. var i,
  6615. iMax,
  6616. ordered = this.ordered,
  6617. options = this.options,
  6618. orientation = options.orientation || this.defaultOptions.orientation,
  6619. axisOnTop = (orientation == 'top'),
  6620. margin;
  6621. if (options.margin && options.margin.item !== undefined) {
  6622. margin = options.margin.item;
  6623. }
  6624. else {
  6625. margin = this.defaultOptions.margin.item
  6626. }
  6627. // calculate new, non-overlapping positions
  6628. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  6629. var item = ordered[i];
  6630. var collidingItem = null;
  6631. do {
  6632. // TODO: optimize checking for overlap. when there is a gap without items,
  6633. // you only need to check for items from the next item on, not from zero
  6634. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  6635. if (collidingItem != null) {
  6636. // There is a collision. Reposition the event above the colliding element
  6637. if (axisOnTop) {
  6638. item.top = collidingItem.top + collidingItem.height + margin;
  6639. }
  6640. else {
  6641. item.top = collidingItem.top - item.height - margin;
  6642. }
  6643. }
  6644. } while (collidingItem);
  6645. }
  6646. };
  6647. /**
  6648. * Check if the destiny position of given item overlaps with any
  6649. * of the other items from index itemStart to itemEnd.
  6650. * @param {Array} items Array with items
  6651. * @param {int} itemIndex Number of the item to be checked for overlap
  6652. * @param {int} itemStart First item to be checked.
  6653. * @param {int} itemEnd Last item to be checked.
  6654. * @return {Object | null} colliding item, or undefined when no collisions
  6655. * @param {Number} margin A minimum required margin.
  6656. * If margin is provided, the two items will be
  6657. * marked colliding when they overlap or
  6658. * when the margin between the two is smaller than
  6659. * the requested margin.
  6660. */
  6661. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  6662. itemStart, itemEnd, margin) {
  6663. var collision = this.collision;
  6664. // we loop from end to start, as we suppose that the chance of a
  6665. // collision is larger for items at the end, so check these first.
  6666. var a = items[itemIndex];
  6667. for (var i = itemEnd; i >= itemStart; i--) {
  6668. var b = items[i];
  6669. if (collision(a, b, margin)) {
  6670. if (i != itemIndex) {
  6671. return b;
  6672. }
  6673. }
  6674. }
  6675. return null;
  6676. };
  6677. /**
  6678. * Test if the two provided items collide
  6679. * The items must have parameters left, width, top, and height.
  6680. * @param {Component} a The first item
  6681. * @param {Component} b The second item
  6682. * @param {Number} margin A minimum required margin.
  6683. * If margin is provided, the two items will be
  6684. * marked colliding when they overlap or
  6685. * when the margin between the two is smaller than
  6686. * the requested margin.
  6687. * @return {boolean} true if a and b collide, else false
  6688. */
  6689. Stack.prototype.collision = function collision (a, b, margin) {
  6690. return ((a.left - margin) < (b.left + b.getWidth()) &&
  6691. (a.left + a.getWidth() + margin) > b.left &&
  6692. (a.top - margin) < (b.top + b.height) &&
  6693. (a.top + a.height + margin) > b.top);
  6694. };
  6695. /**
  6696. * @constructor Range
  6697. * A Range controls a numeric range with a start and end value.
  6698. * The Range adjusts the range based on mouse events or programmatic changes,
  6699. * and triggers events when the range is changing or has been changed.
  6700. * @param {Object} [options] See description at Range.setOptions
  6701. * @extends Controller
  6702. */
  6703. function Range(options) {
  6704. this.id = util.randomUUID();
  6705. this.start = null; // Number
  6706. this.end = null; // Number
  6707. this.options = options || {};
  6708. this.setOptions(options);
  6709. }
  6710. /**
  6711. * Set options for the range controller
  6712. * @param {Object} options Available options:
  6713. * {Number} min Minimum value for start
  6714. * {Number} max Maximum value for end
  6715. * {Number} zoomMin Set a minimum value for
  6716. * (end - start).
  6717. * {Number} zoomMax Set a maximum value for
  6718. * (end - start).
  6719. */
  6720. Range.prototype.setOptions = function (options) {
  6721. util.extend(this.options, options);
  6722. // re-apply range with new limitations
  6723. if (this.start !== null && this.end !== null) {
  6724. this.setRange(this.start, this.end);
  6725. }
  6726. };
  6727. /**
  6728. * Test whether direction has a valid value
  6729. * @param {String} direction 'horizontal' or 'vertical'
  6730. */
  6731. function validateDirection (direction) {
  6732. if (direction != 'horizontal' && direction != 'vertical') {
  6733. throw new TypeError('Unknown direction "' + direction + '". ' +
  6734. 'Choose "horizontal" or "vertical".');
  6735. }
  6736. }
  6737. /**
  6738. * Add listeners for mouse and touch events to the component
  6739. * @param {Component} component
  6740. * @param {String} event Available events: 'move', 'zoom'
  6741. * @param {String} direction Available directions: 'horizontal', 'vertical'
  6742. */
  6743. Range.prototype.subscribe = function (component, event, direction) {
  6744. var me = this;
  6745. if (event == 'move') {
  6746. // drag start listener
  6747. component.on('dragstart', function (event) {
  6748. me._onDragStart(event, component);
  6749. });
  6750. // drag listener
  6751. component.on('drag', function (event) {
  6752. me._onDrag(event, component, direction);
  6753. });
  6754. // drag end listener
  6755. component.on('dragend', function (event) {
  6756. me._onDragEnd(event, component);
  6757. });
  6758. }
  6759. else if (event == 'zoom') {
  6760. // mouse wheel
  6761. function mousewheel (event) {
  6762. me._onMouseWheel(event, component, direction);
  6763. }
  6764. component.on('mousewheel', mousewheel);
  6765. component.on('DOMMouseScroll', mousewheel); // For FF
  6766. // pinch
  6767. component.on('touch', function (event) {
  6768. me._onTouch();
  6769. });
  6770. component.on('pinch', function (event) {
  6771. me._onPinch(event, component, direction);
  6772. });
  6773. }
  6774. else {
  6775. throw new TypeError('Unknown event "' + event + '". ' +
  6776. 'Choose "move" or "zoom".');
  6777. }
  6778. };
  6779. /**
  6780. * Event handler
  6781. * @param {String} event name of the event, for example 'click', 'mousemove'
  6782. * @param {function} callback callback handler, invoked with the raw HTML Event
  6783. * as parameter.
  6784. */
  6785. Range.prototype.on = function (event, callback) {
  6786. events.addListener(this, event, callback);
  6787. };
  6788. /**
  6789. * Trigger an event
  6790. * @param {String} event name of the event, available events: 'rangechange',
  6791. * 'rangechanged'
  6792. * @private
  6793. */
  6794. Range.prototype._trigger = function (event) {
  6795. events.trigger(this, event, {
  6796. start: this.start,
  6797. end: this.end
  6798. });
  6799. };
  6800. /**
  6801. * Set a new start and end range
  6802. * @param {Number} [start]
  6803. * @param {Number} [end]
  6804. */
  6805. Range.prototype.setRange = function(start, end) {
  6806. var changed = this._applyRange(start, end);
  6807. if (changed) {
  6808. this._trigger('rangechange');
  6809. this._trigger('rangechanged');
  6810. }
  6811. };
  6812. /**
  6813. * Set a new start and end range. This method is the same as setRange, but
  6814. * does not trigger a range change and range changed event, and it returns
  6815. * true when the range is changed
  6816. * @param {Number} [start]
  6817. * @param {Number} [end]
  6818. * @return {Boolean} changed
  6819. * @private
  6820. */
  6821. Range.prototype._applyRange = function(start, end) {
  6822. var newStart = (start != null) ? util.convert(start, 'Number') : this.start,
  6823. newEnd = (end != null) ? util.convert(end, 'Number') : this.end,
  6824. max = (this.options.max != null) ? util.convert(this.options.max, 'Date').valueOf() : null,
  6825. min = (this.options.min != null) ? util.convert(this.options.min, 'Date').valueOf() : null,
  6826. diff;
  6827. // check for valid number
  6828. if (isNaN(newStart) || newStart === null) {
  6829. throw new Error('Invalid start "' + start + '"');
  6830. }
  6831. if (isNaN(newEnd) || newEnd === null) {
  6832. throw new Error('Invalid end "' + end + '"');
  6833. }
  6834. // prevent start < end
  6835. if (newEnd < newStart) {
  6836. newEnd = newStart;
  6837. }
  6838. // prevent start < min
  6839. if (min !== null) {
  6840. if (newStart < min) {
  6841. diff = (min - newStart);
  6842. newStart += diff;
  6843. newEnd += diff;
  6844. // prevent end > max
  6845. if (max != null) {
  6846. if (newEnd > max) {
  6847. newEnd = max;
  6848. }
  6849. }
  6850. }
  6851. }
  6852. // prevent end > max
  6853. if (max !== null) {
  6854. if (newEnd > max) {
  6855. diff = (newEnd - max);
  6856. newStart -= diff;
  6857. newEnd -= diff;
  6858. // prevent start < min
  6859. if (min != null) {
  6860. if (newStart < min) {
  6861. newStart = min;
  6862. }
  6863. }
  6864. }
  6865. }
  6866. // prevent (end-start) < zoomMin
  6867. if (this.options.zoomMin !== null) {
  6868. var zoomMin = parseFloat(this.options.zoomMin);
  6869. if (zoomMin < 0) {
  6870. zoomMin = 0;
  6871. }
  6872. if ((newEnd - newStart) < zoomMin) {
  6873. if ((this.end - this.start) === zoomMin) {
  6874. // ignore this action, we are already zoomed to the minimum
  6875. newStart = this.start;
  6876. newEnd = this.end;
  6877. }
  6878. else {
  6879. // zoom to the minimum
  6880. diff = (zoomMin - (newEnd - newStart));
  6881. newStart -= diff / 2;
  6882. newEnd += diff / 2;
  6883. }
  6884. }
  6885. }
  6886. // prevent (end-start) > zoomMax
  6887. if (this.options.zoomMax !== null) {
  6888. var zoomMax = parseFloat(this.options.zoomMax);
  6889. if (zoomMax < 0) {
  6890. zoomMax = 0;
  6891. }
  6892. if ((newEnd - newStart) > zoomMax) {
  6893. if ((this.end - this.start) === zoomMax) {
  6894. // ignore this action, we are already zoomed to the maximum
  6895. newStart = this.start;
  6896. newEnd = this.end;
  6897. }
  6898. else {
  6899. // zoom to the maximum
  6900. diff = ((newEnd - newStart) - zoomMax);
  6901. newStart += diff / 2;
  6902. newEnd -= diff / 2;
  6903. }
  6904. }
  6905. }
  6906. var changed = (this.start != newStart || this.end != newEnd);
  6907. this.start = newStart;
  6908. this.end = newEnd;
  6909. return changed;
  6910. };
  6911. /**
  6912. * Retrieve the current range.
  6913. * @return {Object} An object with start and end properties
  6914. */
  6915. Range.prototype.getRange = function() {
  6916. return {
  6917. start: this.start,
  6918. end: this.end
  6919. };
  6920. };
  6921. /**
  6922. * Calculate the conversion offset and scale for current range, based on
  6923. * the provided width
  6924. * @param {Number} width
  6925. * @returns {{offset: number, scale: number}} conversion
  6926. */
  6927. Range.prototype.conversion = function (width) {
  6928. return Range.conversion(this.start, this.end, width);
  6929. };
  6930. /**
  6931. * Static method to calculate the conversion offset and scale for a range,
  6932. * based on the provided start, end, and width
  6933. * @param {Number} start
  6934. * @param {Number} end
  6935. * @param {Number} width
  6936. * @returns {{offset: number, scale: number}} conversion
  6937. */
  6938. Range.conversion = function (start, end, width) {
  6939. if (width != 0 && (end - start != 0)) {
  6940. return {
  6941. offset: start,
  6942. scale: width / (end - start)
  6943. }
  6944. }
  6945. else {
  6946. return {
  6947. offset: 0,
  6948. scale: 1
  6949. };
  6950. }
  6951. };
  6952. // global (private) object to store drag params
  6953. var touchParams = {};
  6954. /**
  6955. * Start dragging horizontally or vertically
  6956. * @param {Event} event
  6957. * @param {Object} component
  6958. * @private
  6959. */
  6960. Range.prototype._onDragStart = function(event, component) {
  6961. // refuse to drag when we where pinching to prevent the timeline make a jump
  6962. // when releasing the fingers in opposite order from the touch screen
  6963. if (touchParams.pinching) return;
  6964. touchParams.start = this.start;
  6965. touchParams.end = this.end;
  6966. var frame = component.frame;
  6967. if (frame) {
  6968. frame.style.cursor = 'move';
  6969. }
  6970. };
  6971. /**
  6972. * Perform dragging operating.
  6973. * @param {Event} event
  6974. * @param {Component} component
  6975. * @param {String} direction 'horizontal' or 'vertical'
  6976. * @private
  6977. */
  6978. Range.prototype._onDrag = function (event, component, direction) {
  6979. validateDirection(direction);
  6980. // refuse to drag when we where pinching to prevent the timeline make a jump
  6981. // when releasing the fingers in opposite order from the touch screen
  6982. if (touchParams.pinching) return;
  6983. var delta = (direction == 'horizontal') ? event.gesture.deltaX : event.gesture.deltaY,
  6984. interval = (touchParams.end - touchParams.start),
  6985. width = (direction == 'horizontal') ? component.width : component.height,
  6986. diffRange = -delta / width * interval;
  6987. this._applyRange(touchParams.start + diffRange, touchParams.end + diffRange);
  6988. // fire a rangechange event
  6989. this._trigger('rangechange');
  6990. };
  6991. /**
  6992. * Stop dragging operating.
  6993. * @param {event} event
  6994. * @param {Component} component
  6995. * @private
  6996. */
  6997. Range.prototype._onDragEnd = function (event, component) {
  6998. // refuse to drag when we where pinching to prevent the timeline make a jump
  6999. // when releasing the fingers in opposite order from the touch screen
  7000. if (touchParams.pinching) return;
  7001. if (component.frame) {
  7002. component.frame.style.cursor = 'auto';
  7003. }
  7004. // fire a rangechanged event
  7005. this._trigger('rangechanged');
  7006. };
  7007. /**
  7008. * Event handler for mouse wheel event, used to zoom
  7009. * Code from http://adomas.org/javascript-mouse-wheel/
  7010. * @param {Event} event
  7011. * @param {Component} component
  7012. * @param {String} direction 'horizontal' or 'vertical'
  7013. * @private
  7014. */
  7015. Range.prototype._onMouseWheel = function(event, component, direction) {
  7016. validateDirection(direction);
  7017. // retrieve delta
  7018. var delta = 0;
  7019. if (event.wheelDelta) { /* IE/Opera. */
  7020. delta = event.wheelDelta / 120;
  7021. } else if (event.detail) { /* Mozilla case. */
  7022. // In Mozilla, sign of delta is different than in IE.
  7023. // Also, delta is multiple of 3.
  7024. delta = -event.detail / 3;
  7025. }
  7026. // If delta is nonzero, handle it.
  7027. // Basically, delta is now positive if wheel was scrolled up,
  7028. // and negative, if wheel was scrolled down.
  7029. if (delta) {
  7030. // perform the zoom action. Delta is normally 1 or -1
  7031. // adjust a negative delta such that zooming in with delta 0.1
  7032. // equals zooming out with a delta -0.1
  7033. var scale;
  7034. if (delta < 0) {
  7035. scale = 1 - (delta / 5);
  7036. }
  7037. else {
  7038. scale = 1 / (1 + (delta / 5)) ;
  7039. }
  7040. // calculate center, the date to zoom around
  7041. var gesture = util.fakeGesture(this, event),
  7042. pointer = getPointer(gesture.touches[0], component.frame),
  7043. pointerDate = this._pointerToDate(component, direction, pointer);
  7044. this.zoom(scale, pointerDate);
  7045. }
  7046. // Prevent default actions caused by mouse wheel
  7047. // (else the page and timeline both zoom and scroll)
  7048. util.preventDefault(event);
  7049. };
  7050. /**
  7051. * On start of a touch gesture, initialize scale to 1
  7052. * @private
  7053. */
  7054. Range.prototype._onTouch = function () {
  7055. touchParams.start = this.start;
  7056. touchParams.end = this.end;
  7057. touchParams.pinching = false;
  7058. touchParams.center = null;
  7059. };
  7060. /**
  7061. * Handle pinch event
  7062. * @param {Event} event
  7063. * @param {Component} component
  7064. * @param {String} direction 'horizontal' or 'vertical'
  7065. * @private
  7066. */
  7067. Range.prototype._onPinch = function (event, component, direction) {
  7068. touchParams.pinching = true;
  7069. if (event.gesture.touches.length > 1) {
  7070. if (!touchParams.center) {
  7071. touchParams.center = getPointer(event.gesture.center, component.frame);
  7072. }
  7073. var scale = 1 / event.gesture.scale,
  7074. initDate = this._pointerToDate(component, direction, touchParams.center),
  7075. center = getPointer(event.gesture.center, component.frame),
  7076. date = this._pointerToDate(component, direction, center),
  7077. delta = date - initDate; // TODO: utilize delta
  7078. // calculate new start and end
  7079. var newStart = parseInt(initDate + (touchParams.start - initDate) * scale);
  7080. var newEnd = parseInt(initDate + (touchParams.end - initDate) * scale);
  7081. // apply new range
  7082. this.setRange(newStart, newEnd);
  7083. }
  7084. };
  7085. /**
  7086. * Helper function to calculate the center date for zooming
  7087. * @param {Component} component
  7088. * @param {{x: Number, y: Number}} pointer
  7089. * @param {String} direction 'horizontal' or 'vertical'
  7090. * @return {number} date
  7091. * @private
  7092. */
  7093. Range.prototype._pointerToDate = function (component, direction, pointer) {
  7094. var conversion;
  7095. if (direction == 'horizontal') {
  7096. var width = component.width;
  7097. conversion = this.conversion(width);
  7098. return pointer.x / conversion.scale + conversion.offset;
  7099. }
  7100. else {
  7101. var height = component.height;
  7102. conversion = this.conversion(height);
  7103. return pointer.y / conversion.scale + conversion.offset;
  7104. }
  7105. };
  7106. /**
  7107. * Get the pointer location relative to the location of the dom element
  7108. * @param {{pageX: Number, pageY: Number}} touch
  7109. * @param {Element} element HTML DOM element
  7110. * @return {{x: Number, y: Number}} pointer
  7111. * @private
  7112. */
  7113. function getPointer (touch, element) {
  7114. return {
  7115. x: touch.pageX - vis.util.getAbsoluteLeft(element),
  7116. y: touch.pageY - vis.util.getAbsoluteTop(element)
  7117. };
  7118. }
  7119. /**
  7120. * Zoom the range the given scale in or out. Start and end date will
  7121. * be adjusted, and the timeline will be redrawn. You can optionally give a
  7122. * date around which to zoom.
  7123. * For example, try scale = 0.9 or 1.1
  7124. * @param {Number} scale Scaling factor. Values above 1 will zoom out,
  7125. * values below 1 will zoom in.
  7126. * @param {Number} [center] Value representing a date around which will
  7127. * be zoomed.
  7128. */
  7129. Range.prototype.zoom = function(scale, center) {
  7130. // if centerDate is not provided, take it half between start Date and end Date
  7131. if (center == null) {
  7132. center = (this.start + this.end) / 2;
  7133. }
  7134. // calculate new start and end
  7135. var newStart = center + (this.start - center) * scale;
  7136. var newEnd = center + (this.end - center) * scale;
  7137. this.setRange(newStart, newEnd);
  7138. };
  7139. /**
  7140. * Move the range with a given delta to the left or right. Start and end
  7141. * value will be adjusted. For example, try delta = 0.1 or -0.1
  7142. * @param {Number} delta Moving amount. Positive value will move right,
  7143. * negative value will move left
  7144. */
  7145. Range.prototype.move = function(delta) {
  7146. // zoom start Date and end Date relative to the centerDate
  7147. var diff = (this.end - this.start);
  7148. // apply new values
  7149. var newStart = this.start + diff * delta;
  7150. var newEnd = this.end + diff * delta;
  7151. // TODO: reckon with min and max range
  7152. this.start = newStart;
  7153. this.end = newEnd;
  7154. };
  7155. /**
  7156. * Move the range to a new center point
  7157. * @param {Number} moveTo New center point of the range
  7158. */
  7159. Range.prototype.moveTo = function(moveTo) {
  7160. var center = (this.start + this.end) / 2;
  7161. var diff = center - moveTo;
  7162. // calculate new start and end
  7163. var newStart = this.start - diff;
  7164. var newEnd = this.end - diff;
  7165. this.setRange(newStart, newEnd);
  7166. };
  7167. /**
  7168. * @constructor Controller
  7169. *
  7170. * A Controller controls the reflows and repaints of all visual components
  7171. */
  7172. function Controller () {
  7173. this.id = util.randomUUID();
  7174. this.components = {};
  7175. this.repaintTimer = undefined;
  7176. this.reflowTimer = undefined;
  7177. }
  7178. /**
  7179. * Add a component to the controller
  7180. * @param {Component} component
  7181. */
  7182. Controller.prototype.add = function add(component) {
  7183. // validate the component
  7184. if (component.id == undefined) {
  7185. throw new Error('Component has no field id');
  7186. }
  7187. if (!(component instanceof Component) && !(component instanceof Controller)) {
  7188. throw new TypeError('Component must be an instance of ' +
  7189. 'prototype Component or Controller');
  7190. }
  7191. // add the component
  7192. component.controller = this;
  7193. this.components[component.id] = component;
  7194. };
  7195. /**
  7196. * Remove a component from the controller
  7197. * @param {Component | String} component
  7198. */
  7199. Controller.prototype.remove = function remove(component) {
  7200. var id;
  7201. for (id in this.components) {
  7202. if (this.components.hasOwnProperty(id)) {
  7203. if (id == component || this.components[id] == component) {
  7204. break;
  7205. }
  7206. }
  7207. }
  7208. if (id) {
  7209. delete this.components[id];
  7210. }
  7211. };
  7212. /**
  7213. * Request a reflow. The controller will schedule a reflow
  7214. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  7215. * is false.
  7216. */
  7217. Controller.prototype.requestReflow = function requestReflow(force) {
  7218. if (force) {
  7219. this.reflow();
  7220. }
  7221. else {
  7222. if (!this.reflowTimer) {
  7223. var me = this;
  7224. this.reflowTimer = setTimeout(function () {
  7225. me.reflowTimer = undefined;
  7226. me.reflow();
  7227. }, 0);
  7228. }
  7229. }
  7230. };
  7231. /**
  7232. * Request a repaint. The controller will schedule a repaint
  7233. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  7234. * is false.
  7235. */
  7236. Controller.prototype.requestRepaint = function requestRepaint(force) {
  7237. if (force) {
  7238. this.repaint();
  7239. }
  7240. else {
  7241. if (!this.repaintTimer) {
  7242. var me = this;
  7243. this.repaintTimer = setTimeout(function () {
  7244. me.repaintTimer = undefined;
  7245. me.repaint();
  7246. }, 0);
  7247. }
  7248. }
  7249. };
  7250. /**
  7251. * Repaint all components
  7252. */
  7253. Controller.prototype.repaint = function repaint() {
  7254. var changed = false;
  7255. // cancel any running repaint request
  7256. if (this.repaintTimer) {
  7257. clearTimeout(this.repaintTimer);
  7258. this.repaintTimer = undefined;
  7259. }
  7260. var done = {};
  7261. function repaint(component, id) {
  7262. if (!(id in done)) {
  7263. // first repaint the components on which this component is dependent
  7264. if (component.depends) {
  7265. component.depends.forEach(function (dep) {
  7266. repaint(dep, dep.id);
  7267. });
  7268. }
  7269. if (component.parent) {
  7270. repaint(component.parent, component.parent.id);
  7271. }
  7272. // repaint the component itself and mark as done
  7273. changed = component.repaint() || changed;
  7274. done[id] = true;
  7275. }
  7276. }
  7277. util.forEach(this.components, repaint);
  7278. // immediately reflow when needed
  7279. if (changed) {
  7280. this.reflow();
  7281. }
  7282. // TODO: limit the number of nested reflows/repaints, prevent loop
  7283. };
  7284. /**
  7285. * Reflow all components
  7286. */
  7287. Controller.prototype.reflow = function reflow() {
  7288. var resized = false;
  7289. // cancel any running repaint request
  7290. if (this.reflowTimer) {
  7291. clearTimeout(this.reflowTimer);
  7292. this.reflowTimer = undefined;
  7293. }
  7294. var done = {};
  7295. function reflow(component, id) {
  7296. if (!(id in done)) {
  7297. // first reflow the components on which this component is dependent
  7298. if (component.depends) {
  7299. component.depends.forEach(function (dep) {
  7300. reflow(dep, dep.id);
  7301. });
  7302. }
  7303. if (component.parent) {
  7304. reflow(component.parent, component.parent.id);
  7305. }
  7306. // reflow the component itself and mark as done
  7307. resized = component.reflow() || resized;
  7308. done[id] = true;
  7309. }
  7310. }
  7311. util.forEach(this.components, reflow);
  7312. // immediately repaint when needed
  7313. if (resized) {
  7314. this.repaint();
  7315. }
  7316. // TODO: limit the number of nested reflows/repaints, prevent loop
  7317. };
  7318. /**
  7319. * Prototype for visual components
  7320. */
  7321. function Component () {
  7322. this.id = null;
  7323. this.parent = null;
  7324. this.depends = null;
  7325. this.controller = null;
  7326. this.options = null;
  7327. this.frame = null; // main DOM element
  7328. this.top = 0;
  7329. this.left = 0;
  7330. this.width = 0;
  7331. this.height = 0;
  7332. }
  7333. /**
  7334. * Set parameters for the frame. Parameters will be merged in current parameter
  7335. * set.
  7336. * @param {Object} options Available parameters:
  7337. * {String | function} [className]
  7338. * {EventBus} [eventBus]
  7339. * {String | Number | function} [left]
  7340. * {String | Number | function} [top]
  7341. * {String | Number | function} [width]
  7342. * {String | Number | function} [height]
  7343. */
  7344. Component.prototype.setOptions = function setOptions(options) {
  7345. if (options) {
  7346. util.extend(this.options, options);
  7347. if (this.controller) {
  7348. this.requestRepaint();
  7349. this.requestReflow();
  7350. }
  7351. }
  7352. };
  7353. /**
  7354. * Get an option value by name
  7355. * The function will first check this.options object, and else will check
  7356. * this.defaultOptions.
  7357. * @param {String} name
  7358. * @return {*} value
  7359. */
  7360. Component.prototype.getOption = function getOption(name) {
  7361. var value;
  7362. if (this.options) {
  7363. value = this.options[name];
  7364. }
  7365. if (value === undefined && this.defaultOptions) {
  7366. value = this.defaultOptions[name];
  7367. }
  7368. return value;
  7369. };
  7370. /**
  7371. * Get the container element of the component, which can be used by a child to
  7372. * add its own widgets. Not all components do have a container for childs, in
  7373. * that case null is returned.
  7374. * @returns {HTMLElement | null} container
  7375. */
  7376. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  7377. Component.prototype.getContainer = function getContainer() {
  7378. // should be implemented by the component
  7379. return null;
  7380. };
  7381. /**
  7382. * Get the frame element of the component, the outer HTML DOM element.
  7383. * @returns {HTMLElement | null} frame
  7384. */
  7385. Component.prototype.getFrame = function getFrame() {
  7386. return this.frame;
  7387. };
  7388. /**
  7389. * Repaint the component
  7390. * @return {Boolean} changed
  7391. */
  7392. Component.prototype.repaint = function repaint() {
  7393. // should be implemented by the component
  7394. return false;
  7395. };
  7396. /**
  7397. * Reflow the component
  7398. * @return {Boolean} resized
  7399. */
  7400. Component.prototype.reflow = function reflow() {
  7401. // should be implemented by the component
  7402. return false;
  7403. };
  7404. /**
  7405. * Hide the component from the DOM
  7406. * @return {Boolean} changed
  7407. */
  7408. Component.prototype.hide = function hide() {
  7409. if (this.frame && this.frame.parentNode) {
  7410. this.frame.parentNode.removeChild(this.frame);
  7411. return true;
  7412. }
  7413. else {
  7414. return false;
  7415. }
  7416. };
  7417. /**
  7418. * Show the component in the DOM (when not already visible).
  7419. * A repaint will be executed when the component is not visible
  7420. * @return {Boolean} changed
  7421. */
  7422. Component.prototype.show = function show() {
  7423. if (!this.frame || !this.frame.parentNode) {
  7424. return this.repaint();
  7425. }
  7426. else {
  7427. return false;
  7428. }
  7429. };
  7430. /**
  7431. * Request a repaint. The controller will schedule a repaint
  7432. */
  7433. Component.prototype.requestRepaint = function requestRepaint() {
  7434. if (this.controller) {
  7435. this.controller.requestRepaint();
  7436. }
  7437. else {
  7438. throw new Error('Cannot request a repaint: no controller configured');
  7439. // TODO: just do a repaint when no parent is configured?
  7440. }
  7441. };
  7442. /**
  7443. * Request a reflow. The controller will schedule a reflow
  7444. */
  7445. Component.prototype.requestReflow = function requestReflow() {
  7446. if (this.controller) {
  7447. this.controller.requestReflow();
  7448. }
  7449. else {
  7450. throw new Error('Cannot request a reflow: no controller configured');
  7451. // TODO: just do a reflow when no parent is configured?
  7452. }
  7453. };
  7454. /**
  7455. * A panel can contain components
  7456. * @param {Component} [parent]
  7457. * @param {Component[]} [depends] Components on which this components depends
  7458. * (except for the parent)
  7459. * @param {Object} [options] Available parameters:
  7460. * {String | Number | function} [left]
  7461. * {String | Number | function} [top]
  7462. * {String | Number | function} [width]
  7463. * {String | Number | function} [height]
  7464. * {String | function} [className]
  7465. * @constructor Panel
  7466. * @extends Component
  7467. */
  7468. function Panel(parent, depends, options) {
  7469. this.id = util.randomUUID();
  7470. this.parent = parent;
  7471. this.depends = depends;
  7472. this.options = options || {};
  7473. }
  7474. Panel.prototype = new Component();
  7475. /**
  7476. * Set options. Will extend the current options.
  7477. * @param {Object} [options] Available parameters:
  7478. * {String | function} [className]
  7479. * {String | Number | function} [left]
  7480. * {String | Number | function} [top]
  7481. * {String | Number | function} [width]
  7482. * {String | Number | function} [height]
  7483. */
  7484. Panel.prototype.setOptions = Component.prototype.setOptions;
  7485. /**
  7486. * Get the container element of the panel, which can be used by a child to
  7487. * add its own widgets.
  7488. * @returns {HTMLElement} container
  7489. */
  7490. Panel.prototype.getContainer = function () {
  7491. return this.frame;
  7492. };
  7493. /**
  7494. * Repaint the component
  7495. * @return {Boolean} changed
  7496. */
  7497. Panel.prototype.repaint = function () {
  7498. var changed = 0,
  7499. update = util.updateProperty,
  7500. asSize = util.option.asSize,
  7501. options = this.options,
  7502. frame = this.frame;
  7503. if (!frame) {
  7504. frame = document.createElement('div');
  7505. frame.className = 'panel';
  7506. var className = options.className;
  7507. if (className) {
  7508. if (typeof className == 'function') {
  7509. util.addClassName(frame, String(className()));
  7510. }
  7511. else {
  7512. util.addClassName(frame, String(className));
  7513. }
  7514. }
  7515. this.frame = frame;
  7516. changed += 1;
  7517. }
  7518. if (!frame.parentNode) {
  7519. if (!this.parent) {
  7520. throw new Error('Cannot repaint panel: no parent attached');
  7521. }
  7522. var parentContainer = this.parent.getContainer();
  7523. if (!parentContainer) {
  7524. throw new Error('Cannot repaint panel: parent has no container element');
  7525. }
  7526. parentContainer.appendChild(frame);
  7527. changed += 1;
  7528. }
  7529. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  7530. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  7531. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  7532. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  7533. return (changed > 0);
  7534. };
  7535. /**
  7536. * Reflow the component
  7537. * @return {Boolean} resized
  7538. */
  7539. Panel.prototype.reflow = function () {
  7540. var changed = 0,
  7541. update = util.updateProperty,
  7542. frame = this.frame;
  7543. if (frame) {
  7544. changed += update(this, 'top', frame.offsetTop);
  7545. changed += update(this, 'left', frame.offsetLeft);
  7546. changed += update(this, 'width', frame.offsetWidth);
  7547. changed += update(this, 'height', frame.offsetHeight);
  7548. }
  7549. else {
  7550. changed += 1;
  7551. }
  7552. return (changed > 0);
  7553. };
  7554. /**
  7555. * A root panel can hold components. The root panel must be initialized with
  7556. * a DOM element as container.
  7557. * @param {HTMLElement} container
  7558. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  7559. * @constructor RootPanel
  7560. * @extends Panel
  7561. */
  7562. function RootPanel(container, options) {
  7563. this.id = util.randomUUID();
  7564. this.container = container;
  7565. this.options = options || {};
  7566. this.defaultOptions = {
  7567. autoResize: true
  7568. };
  7569. this.listeners = {}; // event listeners
  7570. }
  7571. RootPanel.prototype = new Panel();
  7572. /**
  7573. * Set options. Will extend the current options.
  7574. * @param {Object} [options] Available parameters:
  7575. * {String | function} [className]
  7576. * {String | Number | function} [left]
  7577. * {String | Number | function} [top]
  7578. * {String | Number | function} [width]
  7579. * {String | Number | function} [height]
  7580. * {Boolean | function} [autoResize]
  7581. */
  7582. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  7583. /**
  7584. * Repaint the component
  7585. * @return {Boolean} changed
  7586. */
  7587. RootPanel.prototype.repaint = function () {
  7588. var changed = 0,
  7589. update = util.updateProperty,
  7590. asSize = util.option.asSize,
  7591. options = this.options,
  7592. frame = this.frame;
  7593. if (!frame) {
  7594. frame = document.createElement('div');
  7595. this.frame = frame;
  7596. changed += 1;
  7597. }
  7598. if (!frame.parentNode) {
  7599. if (!this.container) {
  7600. throw new Error('Cannot repaint root panel: no container attached');
  7601. }
  7602. this.container.appendChild(frame);
  7603. changed += 1;
  7604. }
  7605. frame.className = 'vis timeline rootpanel ' + options.orientation;
  7606. var className = options.className;
  7607. if (className) {
  7608. util.addClassName(frame, util.option.asString(className));
  7609. }
  7610. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  7611. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  7612. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  7613. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  7614. this._updateEventEmitters();
  7615. this._updateWatch();
  7616. return (changed > 0);
  7617. };
  7618. /**
  7619. * Reflow the component
  7620. * @return {Boolean} resized
  7621. */
  7622. RootPanel.prototype.reflow = function () {
  7623. var changed = 0,
  7624. update = util.updateProperty,
  7625. frame = this.frame;
  7626. if (frame) {
  7627. changed += update(this, 'top', frame.offsetTop);
  7628. changed += update(this, 'left', frame.offsetLeft);
  7629. changed += update(this, 'width', frame.offsetWidth);
  7630. changed += update(this, 'height', frame.offsetHeight);
  7631. }
  7632. else {
  7633. changed += 1;
  7634. }
  7635. return (changed > 0);
  7636. };
  7637. /**
  7638. * Update watching for resize, depending on the current option
  7639. * @private
  7640. */
  7641. RootPanel.prototype._updateWatch = function () {
  7642. var autoResize = this.getOption('autoResize');
  7643. if (autoResize) {
  7644. this._watch();
  7645. }
  7646. else {
  7647. this._unwatch();
  7648. }
  7649. };
  7650. /**
  7651. * Watch for changes in the size of the frame. On resize, the Panel will
  7652. * automatically redraw itself.
  7653. * @private
  7654. */
  7655. RootPanel.prototype._watch = function () {
  7656. var me = this;
  7657. this._unwatch();
  7658. var checkSize = function () {
  7659. var autoResize = me.getOption('autoResize');
  7660. if (!autoResize) {
  7661. // stop watching when the option autoResize is changed to false
  7662. me._unwatch();
  7663. return;
  7664. }
  7665. if (me.frame) {
  7666. // check whether the frame is resized
  7667. if ((me.frame.clientWidth != me.width) ||
  7668. (me.frame.clientHeight != me.height)) {
  7669. me.requestReflow();
  7670. }
  7671. }
  7672. };
  7673. // TODO: automatically cleanup the event listener when the frame is deleted
  7674. util.addEventListener(window, 'resize', checkSize);
  7675. this.watchTimer = setInterval(checkSize, 1000);
  7676. };
  7677. /**
  7678. * Stop watching for a resize of the frame.
  7679. * @private
  7680. */
  7681. RootPanel.prototype._unwatch = function () {
  7682. if (this.watchTimer) {
  7683. clearInterval(this.watchTimer);
  7684. this.watchTimer = undefined;
  7685. }
  7686. // TODO: remove event listener on window.resize
  7687. };
  7688. /**
  7689. * Event handler
  7690. * @param {String} event name of the event, for example 'click', 'mousemove'
  7691. * @param {function} callback callback handler, invoked with the raw HTML Event
  7692. * as parameter.
  7693. */
  7694. RootPanel.prototype.on = function (event, callback) {
  7695. // register the listener at this component
  7696. var arr = this.listeners[event];
  7697. if (!arr) {
  7698. arr = [];
  7699. this.listeners[event] = arr;
  7700. }
  7701. arr.push(callback);
  7702. this._updateEventEmitters();
  7703. };
  7704. /**
  7705. * Update the event listeners for all event emitters
  7706. * @private
  7707. */
  7708. RootPanel.prototype._updateEventEmitters = function () {
  7709. if (this.listeners) {
  7710. var me = this;
  7711. util.forEach(this.listeners, function (listeners, event) {
  7712. if (!me.emitters) {
  7713. me.emitters = {};
  7714. }
  7715. if (!(event in me.emitters)) {
  7716. // create event
  7717. var frame = me.frame;
  7718. if (frame) {
  7719. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  7720. var callback = function(event) {
  7721. listeners.forEach(function (listener) {
  7722. // TODO: filter on event target!
  7723. listener(event);
  7724. });
  7725. };
  7726. me.emitters[event] = callback;
  7727. if (!me.hammer) {
  7728. me.hammer = Hammer(frame, {
  7729. prevent_default: true
  7730. });
  7731. }
  7732. me.hammer.on(event, callback);
  7733. }
  7734. }
  7735. });
  7736. // TODO: be able to delete event listeners
  7737. // TODO: be able to move event listeners to a parent when available
  7738. }
  7739. };
  7740. /**
  7741. * A horizontal time axis
  7742. * @param {Component} parent
  7743. * @param {Component[]} [depends] Components on which this components depends
  7744. * (except for the parent)
  7745. * @param {Object} [options] See TimeAxis.setOptions for the available
  7746. * options.
  7747. * @constructor TimeAxis
  7748. * @extends Component
  7749. */
  7750. function TimeAxis (parent, depends, options) {
  7751. this.id = util.randomUUID();
  7752. this.parent = parent;
  7753. this.depends = depends;
  7754. this.dom = {
  7755. majorLines: [],
  7756. majorTexts: [],
  7757. minorLines: [],
  7758. minorTexts: [],
  7759. redundant: {
  7760. majorLines: [],
  7761. majorTexts: [],
  7762. minorLines: [],
  7763. minorTexts: []
  7764. }
  7765. };
  7766. this.props = {
  7767. range: {
  7768. start: 0,
  7769. end: 0,
  7770. minimumStep: 0
  7771. },
  7772. lineTop: 0
  7773. };
  7774. this.options = options || {};
  7775. this.defaultOptions = {
  7776. orientation: 'bottom', // supported: 'top', 'bottom'
  7777. // TODO: implement timeaxis orientations 'left' and 'right'
  7778. showMinorLabels: true,
  7779. showMajorLabels: true
  7780. };
  7781. this.conversion = null;
  7782. this.range = null;
  7783. }
  7784. TimeAxis.prototype = new Component();
  7785. // TODO: comment options
  7786. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  7787. /**
  7788. * Set a range (start and end)
  7789. * @param {Range | Object} range A Range or an object containing start and end.
  7790. */
  7791. TimeAxis.prototype.setRange = function (range) {
  7792. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  7793. throw new TypeError('Range must be an instance of Range, ' +
  7794. 'or an object containing start and end.');
  7795. }
  7796. this.range = range;
  7797. };
  7798. /**
  7799. * Convert a position on screen (pixels) to a datetime
  7800. * @param {int} x Position on the screen in pixels
  7801. * @return {Date} time The datetime the corresponds with given position x
  7802. */
  7803. TimeAxis.prototype.toTime = function(x) {
  7804. var conversion = this.conversion;
  7805. return new Date(x / conversion.scale + conversion.offset);
  7806. };
  7807. /**
  7808. * Convert a datetime (Date object) into a position on the screen
  7809. * @param {Date} time A date
  7810. * @return {int} x The position on the screen in pixels which corresponds
  7811. * with the given date.
  7812. * @private
  7813. */
  7814. TimeAxis.prototype.toScreen = function(time) {
  7815. var conversion = this.conversion;
  7816. return (time.valueOf() - conversion.offset) * conversion.scale;
  7817. };
  7818. /**
  7819. * Repaint the component
  7820. * @return {Boolean} changed
  7821. */
  7822. TimeAxis.prototype.repaint = function () {
  7823. var changed = 0,
  7824. update = util.updateProperty,
  7825. asSize = util.option.asSize,
  7826. options = this.options,
  7827. orientation = this.getOption('orientation'),
  7828. props = this.props,
  7829. step = this.step;
  7830. var frame = this.frame;
  7831. if (!frame) {
  7832. frame = document.createElement('div');
  7833. this.frame = frame;
  7834. changed += 1;
  7835. }
  7836. frame.className = 'axis';
  7837. // TODO: custom className?
  7838. if (!frame.parentNode) {
  7839. if (!this.parent) {
  7840. throw new Error('Cannot repaint time axis: no parent attached');
  7841. }
  7842. var parentContainer = this.parent.getContainer();
  7843. if (!parentContainer) {
  7844. throw new Error('Cannot repaint time axis: parent has no container element');
  7845. }
  7846. parentContainer.appendChild(frame);
  7847. changed += 1;
  7848. }
  7849. var parent = frame.parentNode;
  7850. if (parent) {
  7851. var beforeChild = frame.nextSibling;
  7852. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  7853. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  7854. (this.props.parentHeight - this.height) + 'px' :
  7855. '0px';
  7856. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  7857. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  7858. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  7859. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  7860. // get characters width and height
  7861. this._repaintMeasureChars();
  7862. if (this.step) {
  7863. this._repaintStart();
  7864. step.first();
  7865. var xFirstMajorLabel = undefined;
  7866. var max = 0;
  7867. while (step.hasNext() && max < 1000) {
  7868. max++;
  7869. var cur = step.getCurrent(),
  7870. x = this.toScreen(cur),
  7871. isMajor = step.isMajor();
  7872. // TODO: lines must have a width, such that we can create css backgrounds
  7873. if (this.getOption('showMinorLabels')) {
  7874. this._repaintMinorText(x, step.getLabelMinor());
  7875. }
  7876. if (isMajor && this.getOption('showMajorLabels')) {
  7877. if (x > 0) {
  7878. if (xFirstMajorLabel == undefined) {
  7879. xFirstMajorLabel = x;
  7880. }
  7881. this._repaintMajorText(x, step.getLabelMajor());
  7882. }
  7883. this._repaintMajorLine(x);
  7884. }
  7885. else {
  7886. this._repaintMinorLine(x);
  7887. }
  7888. step.next();
  7889. }
  7890. // create a major label on the left when needed
  7891. if (this.getOption('showMajorLabels')) {
  7892. var leftTime = this.toTime(0),
  7893. leftText = step.getLabelMajor(leftTime),
  7894. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  7895. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  7896. this._repaintMajorText(0, leftText);
  7897. }
  7898. }
  7899. this._repaintEnd();
  7900. }
  7901. this._repaintLine();
  7902. // put frame online again
  7903. if (beforeChild) {
  7904. parent.insertBefore(frame, beforeChild);
  7905. }
  7906. else {
  7907. parent.appendChild(frame)
  7908. }
  7909. }
  7910. return (changed > 0);
  7911. };
  7912. /**
  7913. * Start a repaint. Move all DOM elements to a redundant list, where they
  7914. * can be picked for re-use, or can be cleaned up in the end
  7915. * @private
  7916. */
  7917. TimeAxis.prototype._repaintStart = function () {
  7918. var dom = this.dom,
  7919. redundant = dom.redundant;
  7920. redundant.majorLines = dom.majorLines;
  7921. redundant.majorTexts = dom.majorTexts;
  7922. redundant.minorLines = dom.minorLines;
  7923. redundant.minorTexts = dom.minorTexts;
  7924. dom.majorLines = [];
  7925. dom.majorTexts = [];
  7926. dom.minorLines = [];
  7927. dom.minorTexts = [];
  7928. };
  7929. /**
  7930. * End a repaint. Cleanup leftover DOM elements in the redundant list
  7931. * @private
  7932. */
  7933. TimeAxis.prototype._repaintEnd = function () {
  7934. util.forEach(this.dom.redundant, function (arr) {
  7935. while (arr.length) {
  7936. var elem = arr.pop();
  7937. if (elem && elem.parentNode) {
  7938. elem.parentNode.removeChild(elem);
  7939. }
  7940. }
  7941. });
  7942. };
  7943. /**
  7944. * Create a minor label for the axis at position x
  7945. * @param {Number} x
  7946. * @param {String} text
  7947. * @private
  7948. */
  7949. TimeAxis.prototype._repaintMinorText = function (x, text) {
  7950. // reuse redundant label
  7951. var label = this.dom.redundant.minorTexts.shift();
  7952. if (!label) {
  7953. // create new label
  7954. var content = document.createTextNode('');
  7955. label = document.createElement('div');
  7956. label.appendChild(content);
  7957. label.className = 'text minor';
  7958. this.frame.appendChild(label);
  7959. }
  7960. this.dom.minorTexts.push(label);
  7961. label.childNodes[0].nodeValue = text;
  7962. label.style.left = x + 'px';
  7963. label.style.top = this.props.minorLabelTop + 'px';
  7964. //label.title = title; // TODO: this is a heavy operation
  7965. };
  7966. /**
  7967. * Create a Major label for the axis at position x
  7968. * @param {Number} x
  7969. * @param {String} text
  7970. * @private
  7971. */
  7972. TimeAxis.prototype._repaintMajorText = function (x, text) {
  7973. // reuse redundant label
  7974. var label = this.dom.redundant.majorTexts.shift();
  7975. if (!label) {
  7976. // create label
  7977. var content = document.createTextNode(text);
  7978. label = document.createElement('div');
  7979. label.className = 'text major';
  7980. label.appendChild(content);
  7981. this.frame.appendChild(label);
  7982. }
  7983. this.dom.majorTexts.push(label);
  7984. label.childNodes[0].nodeValue = text;
  7985. label.style.top = this.props.majorLabelTop + 'px';
  7986. label.style.left = x + 'px';
  7987. //label.title = title; // TODO: this is a heavy operation
  7988. };
  7989. /**
  7990. * Create a minor line for the axis at position x
  7991. * @param {Number} x
  7992. * @private
  7993. */
  7994. TimeAxis.prototype._repaintMinorLine = function (x) {
  7995. // reuse redundant line
  7996. var line = this.dom.redundant.minorLines.shift();
  7997. if (!line) {
  7998. // create vertical line
  7999. line = document.createElement('div');
  8000. line.className = 'grid vertical minor';
  8001. this.frame.appendChild(line);
  8002. }
  8003. this.dom.minorLines.push(line);
  8004. var props = this.props;
  8005. line.style.top = props.minorLineTop + 'px';
  8006. line.style.height = props.minorLineHeight + 'px';
  8007. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  8008. };
  8009. /**
  8010. * Create a Major line for the axis at position x
  8011. * @param {Number} x
  8012. * @private
  8013. */
  8014. TimeAxis.prototype._repaintMajorLine = function (x) {
  8015. // reuse redundant line
  8016. var line = this.dom.redundant.majorLines.shift();
  8017. if (!line) {
  8018. // create vertical line
  8019. line = document.createElement('DIV');
  8020. line.className = 'grid vertical major';
  8021. this.frame.appendChild(line);
  8022. }
  8023. this.dom.majorLines.push(line);
  8024. var props = this.props;
  8025. line.style.top = props.majorLineTop + 'px';
  8026. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  8027. line.style.height = props.majorLineHeight + 'px';
  8028. };
  8029. /**
  8030. * Repaint the horizontal line for the axis
  8031. * @private
  8032. */
  8033. TimeAxis.prototype._repaintLine = function() {
  8034. var line = this.dom.line,
  8035. frame = this.frame,
  8036. options = this.options;
  8037. // line before all axis elements
  8038. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  8039. if (line) {
  8040. // put this line at the end of all childs
  8041. frame.removeChild(line);
  8042. frame.appendChild(line);
  8043. }
  8044. else {
  8045. // create the axis line
  8046. line = document.createElement('div');
  8047. line.className = 'grid horizontal major';
  8048. frame.appendChild(line);
  8049. this.dom.line = line;
  8050. }
  8051. line.style.top = this.props.lineTop + 'px';
  8052. }
  8053. else {
  8054. if (line && line.parentElement) {
  8055. frame.removeChild(line.line);
  8056. delete this.dom.line;
  8057. }
  8058. }
  8059. };
  8060. /**
  8061. * Create characters used to determine the size of text on the axis
  8062. * @private
  8063. */
  8064. TimeAxis.prototype._repaintMeasureChars = function () {
  8065. // calculate the width and height of a single character
  8066. // this is used to calculate the step size, and also the positioning of the
  8067. // axis
  8068. var dom = this.dom,
  8069. text;
  8070. if (!dom.measureCharMinor) {
  8071. text = document.createTextNode('0');
  8072. var measureCharMinor = document.createElement('DIV');
  8073. measureCharMinor.className = 'text minor measure';
  8074. measureCharMinor.appendChild(text);
  8075. this.frame.appendChild(measureCharMinor);
  8076. dom.measureCharMinor = measureCharMinor;
  8077. }
  8078. if (!dom.measureCharMajor) {
  8079. text = document.createTextNode('0');
  8080. var measureCharMajor = document.createElement('DIV');
  8081. measureCharMajor.className = 'text major measure';
  8082. measureCharMajor.appendChild(text);
  8083. this.frame.appendChild(measureCharMajor);
  8084. dom.measureCharMajor = measureCharMajor;
  8085. }
  8086. };
  8087. /**
  8088. * Reflow the component
  8089. * @return {Boolean} resized
  8090. */
  8091. TimeAxis.prototype.reflow = function () {
  8092. var changed = 0,
  8093. update = util.updateProperty,
  8094. frame = this.frame,
  8095. range = this.range;
  8096. if (!range) {
  8097. throw new Error('Cannot repaint time axis: no range configured');
  8098. }
  8099. if (frame) {
  8100. changed += update(this, 'top', frame.offsetTop);
  8101. changed += update(this, 'left', frame.offsetLeft);
  8102. // calculate size of a character
  8103. var props = this.props,
  8104. showMinorLabels = this.getOption('showMinorLabels'),
  8105. showMajorLabels = this.getOption('showMajorLabels'),
  8106. measureCharMinor = this.dom.measureCharMinor,
  8107. measureCharMajor = this.dom.measureCharMajor;
  8108. if (measureCharMinor) {
  8109. props.minorCharHeight = measureCharMinor.clientHeight;
  8110. props.minorCharWidth = measureCharMinor.clientWidth;
  8111. }
  8112. if (measureCharMajor) {
  8113. props.majorCharHeight = measureCharMajor.clientHeight;
  8114. props.majorCharWidth = measureCharMajor.clientWidth;
  8115. }
  8116. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  8117. if (parentHeight != props.parentHeight) {
  8118. props.parentHeight = parentHeight;
  8119. changed += 1;
  8120. }
  8121. switch (this.getOption('orientation')) {
  8122. case 'bottom':
  8123. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  8124. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  8125. props.minorLabelTop = 0;
  8126. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  8127. props.minorLineTop = -this.top;
  8128. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  8129. props.minorLineWidth = 1; // TODO: really calculate width
  8130. props.majorLineTop = -this.top;
  8131. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  8132. props.majorLineWidth = 1; // TODO: really calculate width
  8133. props.lineTop = 0;
  8134. break;
  8135. case 'top':
  8136. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  8137. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  8138. props.majorLabelTop = 0;
  8139. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  8140. props.minorLineTop = props.minorLabelTop;
  8141. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  8142. props.minorLineWidth = 1; // TODO: really calculate width
  8143. props.majorLineTop = 0;
  8144. props.majorLineHeight = Math.max(parentHeight - this.top);
  8145. props.majorLineWidth = 1; // TODO: really calculate width
  8146. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  8147. break;
  8148. default:
  8149. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  8150. }
  8151. var height = props.minorLabelHeight + props.majorLabelHeight;
  8152. changed += update(this, 'width', frame.offsetWidth);
  8153. changed += update(this, 'height', height);
  8154. // calculate range and step
  8155. this._updateConversion();
  8156. var start = util.convert(range.start, 'Number'),
  8157. end = util.convert(range.end, 'Number'),
  8158. minimumStep = this.toTime((props.minorCharWidth || 10) * 5).valueOf()
  8159. -this.toTime(0).valueOf();
  8160. this.step = new TimeStep(new Date(start), new Date(end), minimumStep);
  8161. changed += update(props.range, 'start', start);
  8162. changed += update(props.range, 'end', end);
  8163. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  8164. }
  8165. return (changed > 0);
  8166. };
  8167. /**
  8168. * Calculate the scale and offset to convert a position on screen to the
  8169. * corresponding date and vice versa.
  8170. * After the method _updateConversion is executed once, the methods toTime
  8171. * and toScreen can be used.
  8172. * @private
  8173. */
  8174. TimeAxis.prototype._updateConversion = function() {
  8175. var range = this.range;
  8176. if (!range) {
  8177. throw new Error('No range configured');
  8178. }
  8179. if (range.conversion) {
  8180. this.conversion = range.conversion(this.width);
  8181. }
  8182. else {
  8183. this.conversion = Range.conversion(range.start, range.end, this.width);
  8184. }
  8185. };
  8186. /**
  8187. * A current time bar
  8188. * @param {Component} parent
  8189. * @param {Component[]} [depends] Components on which this components depends
  8190. * (except for the parent)
  8191. * @param {Object} [options] Available parameters:
  8192. * {Boolean} [showCurrentTime]
  8193. * @constructor CurrentTime
  8194. * @extends Component
  8195. */
  8196. function CurrentTime (parent, depends, options) {
  8197. this.id = util.randomUUID();
  8198. this.parent = parent;
  8199. this.depends = depends;
  8200. this.options = options || {};
  8201. this.defaultOptions = {
  8202. showCurrentTime: false
  8203. };
  8204. }
  8205. CurrentTime.prototype = new Component();
  8206. CurrentTime.prototype.setOptions = Component.prototype.setOptions;
  8207. /**
  8208. * Get the container element of the bar, which can be used by a child to
  8209. * add its own widgets.
  8210. * @returns {HTMLElement} container
  8211. */
  8212. CurrentTime.prototype.getContainer = function () {
  8213. return this.frame;
  8214. };
  8215. /**
  8216. * Repaint the component
  8217. * @return {Boolean} changed
  8218. */
  8219. CurrentTime.prototype.repaint = function () {
  8220. var bar = this.frame,
  8221. parent = this.parent,
  8222. parentContainer = parent.parent.getContainer();
  8223. if (!parent) {
  8224. throw new Error('Cannot repaint bar: no parent attached');
  8225. }
  8226. if (!parentContainer) {
  8227. throw new Error('Cannot repaint bar: parent has no container element');
  8228. }
  8229. if (!this.getOption('showCurrentTime')) {
  8230. if (bar) {
  8231. parentContainer.removeChild(bar);
  8232. delete this.frame;
  8233. }
  8234. return;
  8235. }
  8236. if (!bar) {
  8237. bar = document.createElement('div');
  8238. bar.className = 'currenttime';
  8239. bar.style.position = 'absolute';
  8240. bar.style.top = '0px';
  8241. bar.style.height = '100%';
  8242. parentContainer.appendChild(bar);
  8243. this.frame = bar;
  8244. }
  8245. if (!parent.conversion) {
  8246. parent._updateConversion();
  8247. }
  8248. var now = new Date();
  8249. var x = parent.toScreen(now);
  8250. bar.style.left = x + 'px';
  8251. bar.title = 'Current time: ' + now;
  8252. // start a timer to adjust for the new time
  8253. if (this.currentTimeTimer !== undefined) {
  8254. clearTimeout(this.currentTimeTimer);
  8255. delete this.currentTimeTimer;
  8256. }
  8257. var timeline = this;
  8258. var interval = 1 / parent.conversion.scale / 2;
  8259. if (interval < 30) {
  8260. interval = 30;
  8261. }
  8262. this.currentTimeTimer = setTimeout(function() {
  8263. timeline.repaint();
  8264. }, interval);
  8265. return false;
  8266. };
  8267. /**
  8268. * A custom time bar
  8269. * @param {Component} parent
  8270. * @param {Component[]} [depends] Components on which this components depends
  8271. * (except for the parent)
  8272. * @param {Object} [options] Available parameters:
  8273. * {Boolean} [showCustomTime]
  8274. * @constructor CustomTime
  8275. * @extends Component
  8276. */
  8277. function CustomTime (parent, depends, options) {
  8278. this.id = util.randomUUID();
  8279. this.parent = parent;
  8280. this.depends = depends;
  8281. this.options = options || {};
  8282. this.defaultOptions = {
  8283. showCustomTime: false
  8284. };
  8285. this.listeners = [];
  8286. this.customTime = new Date();
  8287. }
  8288. CustomTime.prototype = new Component();
  8289. CustomTime.prototype.setOptions = Component.prototype.setOptions;
  8290. /**
  8291. * Get the container element of the bar, which can be used by a child to
  8292. * add its own widgets.
  8293. * @returns {HTMLElement} container
  8294. */
  8295. CustomTime.prototype.getContainer = function () {
  8296. return this.frame;
  8297. };
  8298. /**
  8299. * Repaint the component
  8300. * @return {Boolean} changed
  8301. */
  8302. CustomTime.prototype.repaint = function () {
  8303. var bar = this.frame,
  8304. parent = this.parent,
  8305. parentContainer = parent.parent.getContainer();
  8306. if (!parent) {
  8307. throw new Error('Cannot repaint bar: no parent attached');
  8308. }
  8309. if (!parentContainer) {
  8310. throw new Error('Cannot repaint bar: parent has no container element');
  8311. }
  8312. if (!this.getOption('showCustomTime')) {
  8313. if (bar) {
  8314. parentContainer.removeChild(bar);
  8315. delete this.frame;
  8316. }
  8317. return;
  8318. }
  8319. if (!bar) {
  8320. bar = document.createElement('div');
  8321. bar.className = 'customtime';
  8322. bar.style.position = 'absolute';
  8323. bar.style.top = '0px';
  8324. bar.style.height = '100%';
  8325. parentContainer.appendChild(bar);
  8326. var drag = document.createElement('div');
  8327. drag.style.position = 'relative';
  8328. drag.style.top = '0px';
  8329. drag.style.left = '-10px';
  8330. drag.style.height = '100%';
  8331. drag.style.width = '20px';
  8332. bar.appendChild(drag);
  8333. this.frame = bar;
  8334. this.subscribe(this, 'movetime');
  8335. }
  8336. if (!parent.conversion) {
  8337. parent._updateConversion();
  8338. }
  8339. var x = parent.toScreen(this.customTime);
  8340. bar.style.left = x + 'px';
  8341. bar.title = 'Time: ' + this.customTime;
  8342. return false;
  8343. };
  8344. /**
  8345. * Set custom time.
  8346. * @param {Date} time
  8347. */
  8348. CustomTime.prototype._setCustomTime = function(time) {
  8349. this.customTime = new Date(time.valueOf());
  8350. this.repaint();
  8351. };
  8352. /**
  8353. * Retrieve the current custom time.
  8354. * @return {Date} customTime
  8355. */
  8356. CustomTime.prototype._getCustomTime = function() {
  8357. return new Date(this.customTime.valueOf());
  8358. };
  8359. /**
  8360. * Add listeners for mouse and touch events to the component
  8361. * @param {Component} component
  8362. */
  8363. CustomTime.prototype.subscribe = function (component, event) {
  8364. var me = this;
  8365. var listener = {
  8366. component: component,
  8367. event: event,
  8368. callback: function (event) {
  8369. me._onMouseDown(event, listener);
  8370. },
  8371. params: {}
  8372. };
  8373. component.on('mousedown', listener.callback);
  8374. me.listeners.push(listener);
  8375. };
  8376. /**
  8377. * Event handler
  8378. * @param {String} event name of the event, for example 'click', 'mousemove'
  8379. * @param {function} callback callback handler, invoked with the raw HTML Event
  8380. * as parameter.
  8381. */
  8382. CustomTime.prototype.on = function (event, callback) {
  8383. var bar = this.frame;
  8384. if (!bar) {
  8385. throw new Error('Cannot add event listener: no parent attached');
  8386. }
  8387. events.addListener(this, event, callback);
  8388. util.addEventListener(bar, event, callback);
  8389. };
  8390. /**
  8391. * Start moving horizontally
  8392. * @param {Event} event
  8393. * @param {Object} listener Listener containing the component and params
  8394. * @private
  8395. */
  8396. CustomTime.prototype._onMouseDown = function(event, listener) {
  8397. event = event || window.event;
  8398. var params = listener.params;
  8399. // only react on left mouse button down
  8400. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  8401. if (!leftButtonDown) {
  8402. return;
  8403. }
  8404. // get mouse position
  8405. params.mouseX = util.getPageX(event);
  8406. params.moved = false;
  8407. params.customTime = this.customTime;
  8408. // add event listeners to handle moving the custom time bar
  8409. var me = this;
  8410. if (!params.onMouseMove) {
  8411. params.onMouseMove = function (event) {
  8412. me._onMouseMove(event, listener);
  8413. };
  8414. util.addEventListener(document, 'mousemove', params.onMouseMove);
  8415. }
  8416. if (!params.onMouseUp) {
  8417. params.onMouseUp = function (event) {
  8418. me._onMouseUp(event, listener);
  8419. };
  8420. util.addEventListener(document, 'mouseup', params.onMouseUp);
  8421. }
  8422. util.stopPropagation(event);
  8423. util.preventDefault(event);
  8424. };
  8425. /**
  8426. * Perform moving operating.
  8427. * This function activated from within the funcion CustomTime._onMouseDown().
  8428. * @param {Event} event
  8429. * @param {Object} listener
  8430. * @private
  8431. */
  8432. CustomTime.prototype._onMouseMove = function (event, listener) {
  8433. event = event || window.event;
  8434. var params = listener.params;
  8435. var parent = this.parent;
  8436. // calculate change in mouse position
  8437. var mouseX = util.getPageX(event);
  8438. if (params.mouseX === undefined) {
  8439. params.mouseX = mouseX;
  8440. }
  8441. var diff = mouseX - params.mouseX;
  8442. // if mouse movement is big enough, register it as a "moved" event
  8443. if (Math.abs(diff) >= 1) {
  8444. params.moved = true;
  8445. }
  8446. var x = parent.toScreen(params.customTime);
  8447. var xnew = x + diff;
  8448. var time = parent.toTime(xnew);
  8449. this._setCustomTime(time);
  8450. // fire a timechange event
  8451. events.trigger(this, 'timechange', {customTime: this.customTime});
  8452. util.preventDefault(event);
  8453. };
  8454. /**
  8455. * Stop moving operating.
  8456. * This function activated from within the function CustomTime._onMouseDown().
  8457. * @param {event} event
  8458. * @param {Object} listener
  8459. * @private
  8460. */
  8461. CustomTime.prototype._onMouseUp = function (event, listener) {
  8462. event = event || window.event;
  8463. var params = listener.params;
  8464. // remove event listeners here, important for Safari
  8465. if (params.onMouseMove) {
  8466. util.removeEventListener(document, 'mousemove', params.onMouseMove);
  8467. params.onMouseMove = null;
  8468. }
  8469. if (params.onMouseUp) {
  8470. util.removeEventListener(document, 'mouseup', params.onMouseUp);
  8471. params.onMouseUp = null;
  8472. }
  8473. if (params.moved) {
  8474. // fire a timechanged event
  8475. events.trigger(this, 'timechanged', {customTime: this.customTime});
  8476. }
  8477. };
  8478. /**
  8479. * An ItemSet holds a set of items and ranges which can be displayed in a
  8480. * range. The width is determined by the parent of the ItemSet, and the height
  8481. * is determined by the size of the items.
  8482. * @param {Component} parent
  8483. * @param {Component[]} [depends] Components on which this components depends
  8484. * (except for the parent)
  8485. * @param {Object} [options] See ItemSet.setOptions for the available
  8486. * options.
  8487. * @constructor ItemSet
  8488. * @extends Panel
  8489. */
  8490. // TODO: improve performance by replacing all Array.forEach with a for loop
  8491. function ItemSet(parent, depends, options) {
  8492. this.id = util.randomUUID();
  8493. this.parent = parent;
  8494. this.depends = depends;
  8495. // one options object is shared by this itemset and all its items
  8496. this.options = options || {};
  8497. this.defaultOptions = {
  8498. type: 'box',
  8499. align: 'center',
  8500. orientation: 'bottom',
  8501. margin: {
  8502. axis: 20,
  8503. item: 10
  8504. },
  8505. padding: 5
  8506. };
  8507. this.dom = {};
  8508. var me = this;
  8509. this.itemsData = null; // DataSet
  8510. this.range = null; // Range or Object {start: number, end: number}
  8511. this.listeners = {
  8512. 'add': function (event, params, senderId) {
  8513. if (senderId != me.id) {
  8514. me._onAdd(params.items);
  8515. }
  8516. },
  8517. 'update': function (event, params, senderId) {
  8518. if (senderId != me.id) {
  8519. me._onUpdate(params.items);
  8520. }
  8521. },
  8522. 'remove': function (event, params, senderId) {
  8523. if (senderId != me.id) {
  8524. me._onRemove(params.items);
  8525. }
  8526. }
  8527. };
  8528. this.items = {}; // object with an Item for every data item
  8529. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  8530. this.stack = new Stack(this, Object.create(this.options));
  8531. this.conversion = null;
  8532. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  8533. }
  8534. ItemSet.prototype = new Panel();
  8535. // available item types will be registered here
  8536. ItemSet.types = {
  8537. box: ItemBox,
  8538. range: ItemRange,
  8539. rangeoverflow: ItemRangeOverflow,
  8540. point: ItemPoint
  8541. };
  8542. /**
  8543. * Set options for the ItemSet. Existing options will be extended/overwritten.
  8544. * @param {Object} [options] The following options are available:
  8545. * {String | function} [className]
  8546. * class name for the itemset
  8547. * {String} [type]
  8548. * Default type for the items. Choose from 'box'
  8549. * (default), 'point', or 'range'. The default
  8550. * Style can be overwritten by individual items.
  8551. * {String} align
  8552. * Alignment for the items, only applicable for
  8553. * ItemBox. Choose 'center' (default), 'left', or
  8554. * 'right'.
  8555. * {String} orientation
  8556. * Orientation of the item set. Choose 'top' or
  8557. * 'bottom' (default).
  8558. * {Number} margin.axis
  8559. * Margin between the axis and the items in pixels.
  8560. * Default is 20.
  8561. * {Number} margin.item
  8562. * Margin between items in pixels. Default is 10.
  8563. * {Number} padding
  8564. * Padding of the contents of an item in pixels.
  8565. * Must correspond with the items css. Default is 5.
  8566. */
  8567. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  8568. /**
  8569. * Set range (start and end).
  8570. * @param {Range | Object} range A Range or an object containing start and end.
  8571. */
  8572. ItemSet.prototype.setRange = function setRange(range) {
  8573. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  8574. throw new TypeError('Range must be an instance of Range, ' +
  8575. 'or an object containing start and end.');
  8576. }
  8577. this.range = range;
  8578. };
  8579. /**
  8580. * Repaint the component
  8581. * @return {Boolean} changed
  8582. */
  8583. ItemSet.prototype.repaint = function repaint() {
  8584. var changed = 0,
  8585. update = util.updateProperty,
  8586. asSize = util.option.asSize,
  8587. options = this.options,
  8588. orientation = this.getOption('orientation'),
  8589. defaultOptions = this.defaultOptions,
  8590. frame = this.frame;
  8591. if (!frame) {
  8592. frame = document.createElement('div');
  8593. frame.className = 'itemset';
  8594. var className = options.className;
  8595. if (className) {
  8596. util.addClassName(frame, util.option.asString(className));
  8597. }
  8598. // create background panel
  8599. var background = document.createElement('div');
  8600. background.className = 'background';
  8601. frame.appendChild(background);
  8602. this.dom.background = background;
  8603. // create foreground panel
  8604. var foreground = document.createElement('div');
  8605. foreground.className = 'foreground';
  8606. frame.appendChild(foreground);
  8607. this.dom.foreground = foreground;
  8608. // create axis panel
  8609. var axis = document.createElement('div');
  8610. axis.className = 'itemset-axis';
  8611. //frame.appendChild(axis);
  8612. this.dom.axis = axis;
  8613. this.frame = frame;
  8614. changed += 1;
  8615. }
  8616. if (!this.parent) {
  8617. throw new Error('Cannot repaint itemset: no parent attached');
  8618. }
  8619. var parentContainer = this.parent.getContainer();
  8620. if (!parentContainer) {
  8621. throw new Error('Cannot repaint itemset: parent has no container element');
  8622. }
  8623. if (!frame.parentNode) {
  8624. parentContainer.appendChild(frame);
  8625. changed += 1;
  8626. }
  8627. if (!this.dom.axis.parentNode) {
  8628. parentContainer.appendChild(this.dom.axis);
  8629. changed += 1;
  8630. }
  8631. // reposition frame
  8632. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  8633. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  8634. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  8635. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  8636. // reposition axis
  8637. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  8638. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  8639. if (orientation == 'bottom') {
  8640. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  8641. }
  8642. else { // orientation == 'top'
  8643. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  8644. }
  8645. this._updateConversion();
  8646. var me = this,
  8647. queue = this.queue,
  8648. itemsData = this.itemsData,
  8649. items = this.items,
  8650. dataOptions = {
  8651. // TODO: cleanup
  8652. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  8653. };
  8654. // show/hide added/changed/removed items
  8655. Object.keys(queue).forEach(function (id) {
  8656. //var entry = queue[id];
  8657. var action = queue[id];
  8658. var item = items[id];
  8659. //var item = entry.item;
  8660. //noinspection FallthroughInSwitchStatementJS
  8661. switch (action) {
  8662. case 'add':
  8663. case 'update':
  8664. var itemData = itemsData && itemsData.get(id, dataOptions);
  8665. if (itemData) {
  8666. var type = itemData.type ||
  8667. (itemData.start && itemData.end && 'range') ||
  8668. options.type ||
  8669. 'box';
  8670. var constructor = ItemSet.types[type];
  8671. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  8672. if (item) {
  8673. // update item
  8674. if (!constructor || !(item instanceof constructor)) {
  8675. // item type has changed, hide and delete the item
  8676. changed += item.hide();
  8677. item = null;
  8678. }
  8679. else {
  8680. item.data = itemData; // TODO: create a method item.setData ?
  8681. changed++;
  8682. }
  8683. }
  8684. if (!item) {
  8685. // create item
  8686. if (constructor) {
  8687. item = new constructor(me, itemData, options, defaultOptions);
  8688. changed++;
  8689. }
  8690. else {
  8691. throw new TypeError('Unknown item type "' + type + '"');
  8692. }
  8693. }
  8694. // force a repaint (not only a reposition)
  8695. item.repaint();
  8696. items[id] = item;
  8697. }
  8698. // update queue
  8699. delete queue[id];
  8700. break;
  8701. case 'remove':
  8702. if (item) {
  8703. // remove DOM of the item
  8704. changed += item.hide();
  8705. }
  8706. // update lists
  8707. delete items[id];
  8708. delete queue[id];
  8709. break;
  8710. default:
  8711. console.log('Error: unknown action "' + action + '"');
  8712. }
  8713. });
  8714. // reposition all items. Show items only when in the visible area
  8715. util.forEach(this.items, function (item) {
  8716. if (item.visible) {
  8717. changed += item.show();
  8718. item.reposition();
  8719. }
  8720. else {
  8721. changed += item.hide();
  8722. }
  8723. });
  8724. return (changed > 0);
  8725. };
  8726. /**
  8727. * Get the foreground container element
  8728. * @return {HTMLElement} foreground
  8729. */
  8730. ItemSet.prototype.getForeground = function getForeground() {
  8731. return this.dom.foreground;
  8732. };
  8733. /**
  8734. * Get the background container element
  8735. * @return {HTMLElement} background
  8736. */
  8737. ItemSet.prototype.getBackground = function getBackground() {
  8738. return this.dom.background;
  8739. };
  8740. /**
  8741. * Get the axis container element
  8742. * @return {HTMLElement} axis
  8743. */
  8744. ItemSet.prototype.getAxis = function getAxis() {
  8745. return this.dom.axis;
  8746. };
  8747. /**
  8748. * Reflow the component
  8749. * @return {Boolean} resized
  8750. */
  8751. ItemSet.prototype.reflow = function reflow () {
  8752. var changed = 0,
  8753. options = this.options,
  8754. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  8755. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  8756. update = util.updateProperty,
  8757. asNumber = util.option.asNumber,
  8758. asSize = util.option.asSize,
  8759. frame = this.frame;
  8760. if (frame) {
  8761. this._updateConversion();
  8762. util.forEach(this.items, function (item) {
  8763. changed += item.reflow();
  8764. });
  8765. // TODO: stack.update should be triggered via an event, in stack itself
  8766. // TODO: only update the stack when there are changed items
  8767. this.stack.update();
  8768. var maxHeight = asNumber(options.maxHeight);
  8769. var fixedHeight = (asSize(options.height) != null);
  8770. var height;
  8771. if (fixedHeight) {
  8772. height = frame.offsetHeight;
  8773. }
  8774. else {
  8775. // height is not specified, determine the height from the height and positioned items
  8776. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  8777. if (visibleItems.length) {
  8778. var min = visibleItems[0].top;
  8779. var max = visibleItems[0].top + visibleItems[0].height;
  8780. util.forEach(visibleItems, function (item) {
  8781. min = Math.min(min, item.top);
  8782. max = Math.max(max, (item.top + item.height));
  8783. });
  8784. height = (max - min) + marginAxis + marginItem;
  8785. }
  8786. else {
  8787. height = marginAxis + marginItem;
  8788. }
  8789. }
  8790. if (maxHeight != null) {
  8791. height = Math.min(height, maxHeight);
  8792. }
  8793. changed += update(this, 'height', height);
  8794. // calculate height from items
  8795. changed += update(this, 'top', frame.offsetTop);
  8796. changed += update(this, 'left', frame.offsetLeft);
  8797. changed += update(this, 'width', frame.offsetWidth);
  8798. }
  8799. else {
  8800. changed += 1;
  8801. }
  8802. return (changed > 0);
  8803. };
  8804. /**
  8805. * Hide this component from the DOM
  8806. * @return {Boolean} changed
  8807. */
  8808. ItemSet.prototype.hide = function hide() {
  8809. var changed = false;
  8810. // remove the DOM
  8811. if (this.frame && this.frame.parentNode) {
  8812. this.frame.parentNode.removeChild(this.frame);
  8813. changed = true;
  8814. }
  8815. if (this.dom.axis && this.dom.axis.parentNode) {
  8816. this.dom.axis.parentNode.removeChild(this.dom.axis);
  8817. changed = true;
  8818. }
  8819. return changed;
  8820. };
  8821. /**
  8822. * Set items
  8823. * @param {vis.DataSet | null} items
  8824. */
  8825. ItemSet.prototype.setItems = function setItems(items) {
  8826. var me = this,
  8827. ids,
  8828. oldItemsData = this.itemsData;
  8829. // replace the dataset
  8830. if (!items) {
  8831. this.itemsData = null;
  8832. }
  8833. else if (items instanceof DataSet || items instanceof DataView) {
  8834. this.itemsData = items;
  8835. }
  8836. else {
  8837. throw new TypeError('Data must be an instance of DataSet');
  8838. }
  8839. if (oldItemsData) {
  8840. // unsubscribe from old dataset
  8841. util.forEach(this.listeners, function (callback, event) {
  8842. oldItemsData.unsubscribe(event, callback);
  8843. });
  8844. // remove all drawn items
  8845. ids = oldItemsData.getIds();
  8846. this._onRemove(ids);
  8847. }
  8848. if (this.itemsData) {
  8849. // subscribe to new dataset
  8850. var id = this.id;
  8851. util.forEach(this.listeners, function (callback, event) {
  8852. me.itemsData.subscribe(event, callback, id);
  8853. });
  8854. // draw all new items
  8855. ids = this.itemsData.getIds();
  8856. this._onAdd(ids);
  8857. }
  8858. };
  8859. /**
  8860. * Get the current items items
  8861. * @returns {vis.DataSet | null}
  8862. */
  8863. ItemSet.prototype.getItems = function getItems() {
  8864. return this.itemsData;
  8865. };
  8866. /**
  8867. * Handle updated items
  8868. * @param {Number[]} ids
  8869. * @private
  8870. */
  8871. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  8872. this._toQueue('update', ids);
  8873. };
  8874. /**
  8875. * Handle changed items
  8876. * @param {Number[]} ids
  8877. * @private
  8878. */
  8879. ItemSet.prototype._onAdd = function _onAdd(ids) {
  8880. this._toQueue('add', ids);
  8881. };
  8882. /**
  8883. * Handle removed items
  8884. * @param {Number[]} ids
  8885. * @private
  8886. */
  8887. ItemSet.prototype._onRemove = function _onRemove(ids) {
  8888. this._toQueue('remove', ids);
  8889. };
  8890. /**
  8891. * Put items in the queue to be added/updated/remove
  8892. * @param {String} action can be 'add', 'update', 'remove'
  8893. * @param {Number[]} ids
  8894. */
  8895. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  8896. var queue = this.queue;
  8897. ids.forEach(function (id) {
  8898. queue[id] = action;
  8899. });
  8900. if (this.controller) {
  8901. //this.requestReflow();
  8902. this.requestRepaint();
  8903. }
  8904. };
  8905. /**
  8906. * Calculate the scale and offset to convert a position on screen to the
  8907. * corresponding date and vice versa.
  8908. * After the method _updateConversion is executed once, the methods toTime
  8909. * and toScreen can be used.
  8910. * @private
  8911. */
  8912. ItemSet.prototype._updateConversion = function _updateConversion() {
  8913. var range = this.range;
  8914. if (!range) {
  8915. throw new Error('No range configured');
  8916. }
  8917. if (range.conversion) {
  8918. this.conversion = range.conversion(this.width);
  8919. }
  8920. else {
  8921. this.conversion = Range.conversion(range.start, range.end, this.width);
  8922. }
  8923. };
  8924. /**
  8925. * Convert a position on screen (pixels) to a datetime
  8926. * Before this method can be used, the method _updateConversion must be
  8927. * executed once.
  8928. * @param {int} x Position on the screen in pixels
  8929. * @return {Date} time The datetime the corresponds with given position x
  8930. */
  8931. ItemSet.prototype.toTime = function toTime(x) {
  8932. var conversion = this.conversion;
  8933. return new Date(x / conversion.scale + conversion.offset);
  8934. };
  8935. /**
  8936. * Convert a datetime (Date object) into a position on the screen
  8937. * Before this method can be used, the method _updateConversion must be
  8938. * executed once.
  8939. * @param {Date} time A date
  8940. * @return {int} x The position on the screen in pixels which corresponds
  8941. * with the given date.
  8942. */
  8943. ItemSet.prototype.toScreen = function toScreen(time) {
  8944. var conversion = this.conversion;
  8945. return (time.valueOf() - conversion.offset) * conversion.scale;
  8946. };
  8947. /**
  8948. * @constructor Item
  8949. * @param {ItemSet} parent
  8950. * @param {Object} data Object containing (optional) parameters type,
  8951. * start, end, content, group, className.
  8952. * @param {Object} [options] Options to set initial property values
  8953. * @param {Object} [defaultOptions] default options
  8954. * // TODO: describe available options
  8955. */
  8956. function Item (parent, data, options, defaultOptions) {
  8957. this.parent = parent;
  8958. this.data = data;
  8959. this.dom = null;
  8960. this.options = options || {};
  8961. this.defaultOptions = defaultOptions || {};
  8962. this.selected = false;
  8963. this.visible = false;
  8964. this.top = 0;
  8965. this.left = 0;
  8966. this.width = 0;
  8967. this.height = 0;
  8968. }
  8969. /**
  8970. * Select current item
  8971. */
  8972. Item.prototype.select = function select() {
  8973. this.selected = true;
  8974. };
  8975. /**
  8976. * Unselect current item
  8977. */
  8978. Item.prototype.unselect = function unselect() {
  8979. this.selected = false;
  8980. };
  8981. /**
  8982. * Show the Item in the DOM (when not already visible)
  8983. * @return {Boolean} changed
  8984. */
  8985. Item.prototype.show = function show() {
  8986. return false;
  8987. };
  8988. /**
  8989. * Hide the Item from the DOM (when visible)
  8990. * @return {Boolean} changed
  8991. */
  8992. Item.prototype.hide = function hide() {
  8993. return false;
  8994. };
  8995. /**
  8996. * Repaint the item
  8997. * @return {Boolean} changed
  8998. */
  8999. Item.prototype.repaint = function repaint() {
  9000. // should be implemented by the item
  9001. return false;
  9002. };
  9003. /**
  9004. * Reflow the item
  9005. * @return {Boolean} resized
  9006. */
  9007. Item.prototype.reflow = function reflow() {
  9008. // should be implemented by the item
  9009. return false;
  9010. };
  9011. /**
  9012. * Return the items width
  9013. * @return {Integer} width
  9014. */
  9015. Item.prototype.getWidth = function getWidth() {
  9016. return this.width;
  9017. }
  9018. /**
  9019. * @constructor ItemBox
  9020. * @extends Item
  9021. * @param {ItemSet} parent
  9022. * @param {Object} data Object containing parameters start
  9023. * content, className.
  9024. * @param {Object} [options] Options to set initial property values
  9025. * @param {Object} [defaultOptions] default options
  9026. * // TODO: describe available options
  9027. */
  9028. function ItemBox (parent, data, options, defaultOptions) {
  9029. this.props = {
  9030. dot: {
  9031. left: 0,
  9032. top: 0,
  9033. width: 0,
  9034. height: 0
  9035. },
  9036. line: {
  9037. top: 0,
  9038. left: 0,
  9039. width: 0,
  9040. height: 0
  9041. }
  9042. };
  9043. Item.call(this, parent, data, options, defaultOptions);
  9044. }
  9045. ItemBox.prototype = new Item (null, null);
  9046. /**
  9047. * Select the item
  9048. * @override
  9049. */
  9050. ItemBox.prototype.select = function select() {
  9051. this.selected = true;
  9052. // TODO: select and unselect
  9053. };
  9054. /**
  9055. * Unselect the item
  9056. * @override
  9057. */
  9058. ItemBox.prototype.unselect = function unselect() {
  9059. this.selected = false;
  9060. // TODO: select and unselect
  9061. };
  9062. /**
  9063. * Repaint the item
  9064. * @return {Boolean} changed
  9065. */
  9066. ItemBox.prototype.repaint = function repaint() {
  9067. // TODO: make an efficient repaint
  9068. var changed = false;
  9069. var dom = this.dom;
  9070. if (!dom) {
  9071. this._create();
  9072. dom = this.dom;
  9073. changed = true;
  9074. }
  9075. if (dom) {
  9076. if (!this.parent) {
  9077. throw new Error('Cannot repaint item: no parent attached');
  9078. }
  9079. if (!dom.box.parentNode) {
  9080. var foreground = this.parent.getForeground();
  9081. if (!foreground) {
  9082. throw new Error('Cannot repaint time axis: ' +
  9083. 'parent has no foreground container element');
  9084. }
  9085. foreground.appendChild(dom.box);
  9086. changed = true;
  9087. }
  9088. if (!dom.line.parentNode) {
  9089. var background = this.parent.getBackground();
  9090. if (!background) {
  9091. throw new Error('Cannot repaint time axis: ' +
  9092. 'parent has no background container element');
  9093. }
  9094. background.appendChild(dom.line);
  9095. changed = true;
  9096. }
  9097. if (!dom.dot.parentNode) {
  9098. var axis = this.parent.getAxis();
  9099. if (!background) {
  9100. throw new Error('Cannot repaint time axis: ' +
  9101. 'parent has no axis container element');
  9102. }
  9103. axis.appendChild(dom.dot);
  9104. changed = true;
  9105. }
  9106. // update contents
  9107. if (this.data.content != this.content) {
  9108. this.content = this.data.content;
  9109. if (this.content instanceof Element) {
  9110. dom.content.innerHTML = '';
  9111. dom.content.appendChild(this.content);
  9112. }
  9113. else if (this.data.content != undefined) {
  9114. dom.content.innerHTML = this.content;
  9115. }
  9116. else {
  9117. throw new Error('Property "content" missing in item ' + this.data.id);
  9118. }
  9119. changed = true;
  9120. }
  9121. // update class
  9122. var className = (this.data.className? ' ' + this.data.className : '') +
  9123. (this.selected ? ' selected' : '');
  9124. if (this.className != className) {
  9125. this.className = className;
  9126. dom.box.className = 'item box' + className;
  9127. dom.line.className = 'item line' + className;
  9128. dom.dot.className = 'item dot' + className;
  9129. changed = true;
  9130. }
  9131. }
  9132. return changed;
  9133. };
  9134. /**
  9135. * Show the item in the DOM (when not already visible). The items DOM will
  9136. * be created when needed.
  9137. * @return {Boolean} changed
  9138. */
  9139. ItemBox.prototype.show = function show() {
  9140. if (!this.dom || !this.dom.box.parentNode) {
  9141. return this.repaint();
  9142. }
  9143. else {
  9144. return false;
  9145. }
  9146. };
  9147. /**
  9148. * Hide the item from the DOM (when visible)
  9149. * @return {Boolean} changed
  9150. */
  9151. ItemBox.prototype.hide = function hide() {
  9152. var changed = false,
  9153. dom = this.dom;
  9154. if (dom) {
  9155. if (dom.box.parentNode) {
  9156. dom.box.parentNode.removeChild(dom.box);
  9157. changed = true;
  9158. }
  9159. if (dom.line.parentNode) {
  9160. dom.line.parentNode.removeChild(dom.line);
  9161. }
  9162. if (dom.dot.parentNode) {
  9163. dom.dot.parentNode.removeChild(dom.dot);
  9164. }
  9165. }
  9166. return changed;
  9167. };
  9168. /**
  9169. * Reflow the item: calculate its actual size and position from the DOM
  9170. * @return {boolean} resized returns true if the axis is resized
  9171. * @override
  9172. */
  9173. ItemBox.prototype.reflow = function reflow() {
  9174. var changed = 0,
  9175. update,
  9176. dom,
  9177. props,
  9178. options,
  9179. margin,
  9180. start,
  9181. align,
  9182. orientation,
  9183. top,
  9184. left,
  9185. data,
  9186. range;
  9187. if (this.data.start == undefined) {
  9188. throw new Error('Property "start" missing in item ' + this.data.id);
  9189. }
  9190. data = this.data;
  9191. range = this.parent && this.parent.range;
  9192. if (data && range) {
  9193. // TODO: account for the width of the item
  9194. var interval = (range.end - range.start);
  9195. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  9196. }
  9197. else {
  9198. this.visible = false;
  9199. }
  9200. if (this.visible) {
  9201. dom = this.dom;
  9202. if (dom) {
  9203. update = util.updateProperty;
  9204. props = this.props;
  9205. options = this.options;
  9206. start = this.parent.toScreen(this.data.start);
  9207. align = options.align || this.defaultOptions.align;
  9208. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  9209. orientation = options.orientation || this.defaultOptions.orientation;
  9210. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  9211. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  9212. changed += update(props.line, 'width', dom.line.offsetWidth);
  9213. changed += update(props.line, 'height', dom.line.offsetHeight);
  9214. changed += update(props.line, 'top', dom.line.offsetTop);
  9215. changed += update(this, 'width', dom.box.offsetWidth);
  9216. changed += update(this, 'height', dom.box.offsetHeight);
  9217. if (align == 'right') {
  9218. left = start - this.width;
  9219. }
  9220. else if (align == 'left') {
  9221. left = start;
  9222. }
  9223. else {
  9224. // default or 'center'
  9225. left = start - this.width / 2;
  9226. }
  9227. changed += update(this, 'left', left);
  9228. changed += update(props.line, 'left', start - props.line.width / 2);
  9229. changed += update(props.dot, 'left', start - props.dot.width / 2);
  9230. changed += update(props.dot, 'top', -props.dot.height / 2);
  9231. if (orientation == 'top') {
  9232. top = margin;
  9233. changed += update(this, 'top', top);
  9234. }
  9235. else {
  9236. // default or 'bottom'
  9237. var parentHeight = this.parent.height;
  9238. top = parentHeight - this.height - margin;
  9239. changed += update(this, 'top', top);
  9240. }
  9241. }
  9242. else {
  9243. changed += 1;
  9244. }
  9245. }
  9246. return (changed > 0);
  9247. };
  9248. /**
  9249. * Create an items DOM
  9250. * @private
  9251. */
  9252. ItemBox.prototype._create = function _create() {
  9253. var dom = this.dom;
  9254. if (!dom) {
  9255. this.dom = dom = {};
  9256. // create the box
  9257. dom.box = document.createElement('DIV');
  9258. // className is updated in repaint()
  9259. // contents box (inside the background box). used for making margins
  9260. dom.content = document.createElement('DIV');
  9261. dom.content.className = 'content';
  9262. dom.box.appendChild(dom.content);
  9263. // line to axis
  9264. dom.line = document.createElement('DIV');
  9265. dom.line.className = 'line';
  9266. // dot on axis
  9267. dom.dot = document.createElement('DIV');
  9268. dom.dot.className = 'dot';
  9269. }
  9270. };
  9271. /**
  9272. * Reposition the item, recalculate its left, top, and width, using the current
  9273. * range and size of the items itemset
  9274. * @override
  9275. */
  9276. ItemBox.prototype.reposition = function reposition() {
  9277. var dom = this.dom,
  9278. props = this.props,
  9279. orientation = this.options.orientation || this.defaultOptions.orientation;
  9280. if (dom) {
  9281. var box = dom.box,
  9282. line = dom.line,
  9283. dot = dom.dot;
  9284. box.style.left = this.left + 'px';
  9285. box.style.top = this.top + 'px';
  9286. line.style.left = props.line.left + 'px';
  9287. if (orientation == 'top') {
  9288. line.style.top = 0 + 'px';
  9289. line.style.height = this.top + 'px';
  9290. }
  9291. else {
  9292. // orientation 'bottom'
  9293. line.style.top = (this.top + this.height) + 'px';
  9294. line.style.height = Math.max(this.parent.height - this.top - this.height +
  9295. this.props.dot.height / 2, 0) + 'px';
  9296. }
  9297. dot.style.left = props.dot.left + 'px';
  9298. dot.style.top = props.dot.top + 'px';
  9299. }
  9300. };
  9301. /**
  9302. * @constructor ItemPoint
  9303. * @extends Item
  9304. * @param {ItemSet} parent
  9305. * @param {Object} data Object containing parameters start
  9306. * content, className.
  9307. * @param {Object} [options] Options to set initial property values
  9308. * @param {Object} [defaultOptions] default options
  9309. * // TODO: describe available options
  9310. */
  9311. function ItemPoint (parent, data, options, defaultOptions) {
  9312. this.props = {
  9313. dot: {
  9314. top: 0,
  9315. width: 0,
  9316. height: 0
  9317. },
  9318. content: {
  9319. height: 0,
  9320. marginLeft: 0
  9321. }
  9322. };
  9323. Item.call(this, parent, data, options, defaultOptions);
  9324. }
  9325. ItemPoint.prototype = new Item (null, null);
  9326. /**
  9327. * Select the item
  9328. * @override
  9329. */
  9330. ItemPoint.prototype.select = function select() {
  9331. this.selected = true;
  9332. // TODO: select and unselect
  9333. };
  9334. /**
  9335. * Unselect the item
  9336. * @override
  9337. */
  9338. ItemPoint.prototype.unselect = function unselect() {
  9339. this.selected = false;
  9340. // TODO: select and unselect
  9341. };
  9342. /**
  9343. * Repaint the item
  9344. * @return {Boolean} changed
  9345. */
  9346. ItemPoint.prototype.repaint = function repaint() {
  9347. // TODO: make an efficient repaint
  9348. var changed = false;
  9349. var dom = this.dom;
  9350. if (!dom) {
  9351. this._create();
  9352. dom = this.dom;
  9353. changed = true;
  9354. }
  9355. if (dom) {
  9356. if (!this.parent) {
  9357. throw new Error('Cannot repaint item: no parent attached');
  9358. }
  9359. var foreground = this.parent.getForeground();
  9360. if (!foreground) {
  9361. throw new Error('Cannot repaint time axis: ' +
  9362. 'parent has no foreground container element');
  9363. }
  9364. if (!dom.point.parentNode) {
  9365. foreground.appendChild(dom.point);
  9366. foreground.appendChild(dom.point);
  9367. changed = true;
  9368. }
  9369. // update contents
  9370. if (this.data.content != this.content) {
  9371. this.content = this.data.content;
  9372. if (this.content instanceof Element) {
  9373. dom.content.innerHTML = '';
  9374. dom.content.appendChild(this.content);
  9375. }
  9376. else if (this.data.content != undefined) {
  9377. dom.content.innerHTML = this.content;
  9378. }
  9379. else {
  9380. throw new Error('Property "content" missing in item ' + this.data.id);
  9381. }
  9382. changed = true;
  9383. }
  9384. // update class
  9385. var className = (this.data.className? ' ' + this.data.className : '') +
  9386. (this.selected ? ' selected' : '');
  9387. if (this.className != className) {
  9388. this.className = className;
  9389. dom.point.className = 'item point' + className;
  9390. changed = true;
  9391. }
  9392. }
  9393. return changed;
  9394. };
  9395. /**
  9396. * Show the item in the DOM (when not already visible). The items DOM will
  9397. * be created when needed.
  9398. * @return {Boolean} changed
  9399. */
  9400. ItemPoint.prototype.show = function show() {
  9401. if (!this.dom || !this.dom.point.parentNode) {
  9402. return this.repaint();
  9403. }
  9404. else {
  9405. return false;
  9406. }
  9407. };
  9408. /**
  9409. * Hide the item from the DOM (when visible)
  9410. * @return {Boolean} changed
  9411. */
  9412. ItemPoint.prototype.hide = function hide() {
  9413. var changed = false,
  9414. dom = this.dom;
  9415. if (dom) {
  9416. if (dom.point.parentNode) {
  9417. dom.point.parentNode.removeChild(dom.point);
  9418. changed = true;
  9419. }
  9420. }
  9421. return changed;
  9422. };
  9423. /**
  9424. * Reflow the item: calculate its actual size from the DOM
  9425. * @return {boolean} resized returns true if the axis is resized
  9426. * @override
  9427. */
  9428. ItemPoint.prototype.reflow = function reflow() {
  9429. var changed = 0,
  9430. update,
  9431. dom,
  9432. props,
  9433. options,
  9434. margin,
  9435. orientation,
  9436. start,
  9437. top,
  9438. data,
  9439. range;
  9440. if (this.data.start == undefined) {
  9441. throw new Error('Property "start" missing in item ' + this.data.id);
  9442. }
  9443. data = this.data;
  9444. range = this.parent && this.parent.range;
  9445. if (data && range) {
  9446. // TODO: account for the width of the item
  9447. var interval = (range.end - range.start);
  9448. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  9449. }
  9450. else {
  9451. this.visible = false;
  9452. }
  9453. if (this.visible) {
  9454. dom = this.dom;
  9455. if (dom) {
  9456. update = util.updateProperty;
  9457. props = this.props;
  9458. options = this.options;
  9459. orientation = options.orientation || this.defaultOptions.orientation;
  9460. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  9461. start = this.parent.toScreen(this.data.start);
  9462. changed += update(this, 'width', dom.point.offsetWidth);
  9463. changed += update(this, 'height', dom.point.offsetHeight);
  9464. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  9465. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  9466. changed += update(props.content, 'height', dom.content.offsetHeight);
  9467. if (orientation == 'top') {
  9468. top = margin;
  9469. }
  9470. else {
  9471. // default or 'bottom'
  9472. var parentHeight = this.parent.height;
  9473. top = Math.max(parentHeight - this.height - margin, 0);
  9474. }
  9475. changed += update(this, 'top', top);
  9476. changed += update(this, 'left', start - props.dot.width / 2);
  9477. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  9478. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  9479. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  9480. }
  9481. else {
  9482. changed += 1;
  9483. }
  9484. }
  9485. return (changed > 0);
  9486. };
  9487. /**
  9488. * Create an items DOM
  9489. * @private
  9490. */
  9491. ItemPoint.prototype._create = function _create() {
  9492. var dom = this.dom;
  9493. if (!dom) {
  9494. this.dom = dom = {};
  9495. // background box
  9496. dom.point = document.createElement('div');
  9497. // className is updated in repaint()
  9498. // contents box, right from the dot
  9499. dom.content = document.createElement('div');
  9500. dom.content.className = 'content';
  9501. dom.point.appendChild(dom.content);
  9502. // dot at start
  9503. dom.dot = document.createElement('div');
  9504. dom.dot.className = 'dot';
  9505. dom.point.appendChild(dom.dot);
  9506. }
  9507. };
  9508. /**
  9509. * Reposition the item, recalculate its left, top, and width, using the current
  9510. * range and size of the items itemset
  9511. * @override
  9512. */
  9513. ItemPoint.prototype.reposition = function reposition() {
  9514. var dom = this.dom,
  9515. props = this.props;
  9516. if (dom) {
  9517. dom.point.style.top = this.top + 'px';
  9518. dom.point.style.left = this.left + 'px';
  9519. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  9520. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  9521. dom.dot.style.top = props.dot.top + 'px';
  9522. }
  9523. };
  9524. /**
  9525. * @constructor ItemRange
  9526. * @extends Item
  9527. * @param {ItemSet} parent
  9528. * @param {Object} data Object containing parameters start, end
  9529. * content, className.
  9530. * @param {Object} [options] Options to set initial property values
  9531. * @param {Object} [defaultOptions] default options
  9532. * // TODO: describe available options
  9533. */
  9534. function ItemRange (parent, data, options, defaultOptions) {
  9535. this.props = {
  9536. content: {
  9537. left: 0,
  9538. width: 0
  9539. }
  9540. };
  9541. Item.call(this, parent, data, options, defaultOptions);
  9542. }
  9543. ItemRange.prototype = new Item (null, null);
  9544. /**
  9545. * Select the item
  9546. * @override
  9547. */
  9548. ItemRange.prototype.select = function select() {
  9549. this.selected = true;
  9550. // TODO: select and unselect
  9551. };
  9552. /**
  9553. * Unselect the item
  9554. * @override
  9555. */
  9556. ItemRange.prototype.unselect = function unselect() {
  9557. this.selected = false;
  9558. // TODO: select and unselect
  9559. };
  9560. /**
  9561. * Repaint the item
  9562. * @return {Boolean} changed
  9563. */
  9564. ItemRange.prototype.repaint = function repaint() {
  9565. // TODO: make an efficient repaint
  9566. var changed = false;
  9567. var dom = this.dom;
  9568. if (!dom) {
  9569. this._create();
  9570. dom = this.dom;
  9571. changed = true;
  9572. }
  9573. if (dom) {
  9574. if (!this.parent) {
  9575. throw new Error('Cannot repaint item: no parent attached');
  9576. }
  9577. var foreground = this.parent.getForeground();
  9578. if (!foreground) {
  9579. throw new Error('Cannot repaint time axis: ' +
  9580. 'parent has no foreground container element');
  9581. }
  9582. if (!dom.box.parentNode) {
  9583. foreground.appendChild(dom.box);
  9584. changed = true;
  9585. }
  9586. // update content
  9587. if (this.data.content != this.content) {
  9588. this.content = this.data.content;
  9589. if (this.content instanceof Element) {
  9590. dom.content.innerHTML = '';
  9591. dom.content.appendChild(this.content);
  9592. }
  9593. else if (this.data.content != undefined) {
  9594. dom.content.innerHTML = this.content;
  9595. }
  9596. else {
  9597. throw new Error('Property "content" missing in item ' + this.data.id);
  9598. }
  9599. changed = true;
  9600. }
  9601. // update class
  9602. var className = this.data.className ? (' ' + this.data.className) : '';
  9603. if (this.className != className) {
  9604. this.className = className;
  9605. dom.box.className = 'item range' + className;
  9606. changed = true;
  9607. }
  9608. }
  9609. return changed;
  9610. };
  9611. /**
  9612. * Show the item in the DOM (when not already visible). The items DOM will
  9613. * be created when needed.
  9614. * @return {Boolean} changed
  9615. */
  9616. ItemRange.prototype.show = function show() {
  9617. if (!this.dom || !this.dom.box.parentNode) {
  9618. return this.repaint();
  9619. }
  9620. else {
  9621. return false;
  9622. }
  9623. };
  9624. /**
  9625. * Hide the item from the DOM (when visible)
  9626. * @return {Boolean} changed
  9627. */
  9628. ItemRange.prototype.hide = function hide() {
  9629. var changed = false,
  9630. dom = this.dom;
  9631. if (dom) {
  9632. if (dom.box.parentNode) {
  9633. dom.box.parentNode.removeChild(dom.box);
  9634. changed = true;
  9635. }
  9636. }
  9637. return changed;
  9638. };
  9639. /**
  9640. * Reflow the item: calculate its actual size from the DOM
  9641. * @return {boolean} resized returns true if the axis is resized
  9642. * @override
  9643. */
  9644. ItemRange.prototype.reflow = function reflow() {
  9645. var changed = 0,
  9646. dom,
  9647. props,
  9648. options,
  9649. margin,
  9650. padding,
  9651. parent,
  9652. start,
  9653. end,
  9654. data,
  9655. range,
  9656. update,
  9657. box,
  9658. parentWidth,
  9659. contentLeft,
  9660. orientation,
  9661. top;
  9662. if (this.data.start == undefined) {
  9663. throw new Error('Property "start" missing in item ' + this.data.id);
  9664. }
  9665. if (this.data.end == undefined) {
  9666. throw new Error('Property "end" missing in item ' + this.data.id);
  9667. }
  9668. data = this.data;
  9669. range = this.parent && this.parent.range;
  9670. if (data && range) {
  9671. // TODO: account for the width of the item. Take some margin
  9672. this.visible = (data.start < range.end) && (data.end > range.start);
  9673. }
  9674. else {
  9675. this.visible = false;
  9676. }
  9677. if (this.visible) {
  9678. dom = this.dom;
  9679. if (dom) {
  9680. props = this.props;
  9681. options = this.options;
  9682. parent = this.parent;
  9683. start = parent.toScreen(this.data.start);
  9684. end = parent.toScreen(this.data.end);
  9685. update = util.updateProperty;
  9686. box = dom.box;
  9687. parentWidth = parent.width;
  9688. orientation = options.orientation || this.defaultOptions.orientation;
  9689. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  9690. padding = options.padding || this.defaultOptions.padding;
  9691. changed += update(props.content, 'width', dom.content.offsetWidth);
  9692. changed += update(this, 'height', box.offsetHeight);
  9693. // limit the width of the this, as browsers cannot draw very wide divs
  9694. if (start < -parentWidth) {
  9695. start = -parentWidth;
  9696. }
  9697. if (end > 2 * parentWidth) {
  9698. end = 2 * parentWidth;
  9699. }
  9700. // when range exceeds left of the window, position the contents at the left of the visible area
  9701. if (start < 0) {
  9702. contentLeft = Math.min(-start,
  9703. (end - start - props.content.width - 2 * padding));
  9704. // TODO: remove the need for options.padding. it's terrible.
  9705. }
  9706. else {
  9707. contentLeft = 0;
  9708. }
  9709. changed += update(props.content, 'left', contentLeft);
  9710. if (orientation == 'top') {
  9711. top = margin;
  9712. changed += update(this, 'top', top);
  9713. }
  9714. else {
  9715. // default or 'bottom'
  9716. top = parent.height - this.height - margin;
  9717. changed += update(this, 'top', top);
  9718. }
  9719. changed += update(this, 'left', start);
  9720. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  9721. }
  9722. else {
  9723. changed += 1;
  9724. }
  9725. }
  9726. return (changed > 0);
  9727. };
  9728. /**
  9729. * Create an items DOM
  9730. * @private
  9731. */
  9732. ItemRange.prototype._create = function _create() {
  9733. var dom = this.dom;
  9734. if (!dom) {
  9735. this.dom = dom = {};
  9736. // background box
  9737. dom.box = document.createElement('div');
  9738. // className is updated in repaint()
  9739. // contents box
  9740. dom.content = document.createElement('div');
  9741. dom.content.className = 'content';
  9742. dom.box.appendChild(dom.content);
  9743. }
  9744. };
  9745. /**
  9746. * Reposition the item, recalculate its left, top, and width, using the current
  9747. * range and size of the items itemset
  9748. * @override
  9749. */
  9750. ItemRange.prototype.reposition = function reposition() {
  9751. var dom = this.dom,
  9752. props = this.props;
  9753. if (dom) {
  9754. dom.box.style.top = this.top + 'px';
  9755. dom.box.style.left = this.left + 'px';
  9756. dom.box.style.width = this.width + 'px';
  9757. dom.content.style.left = props.content.left + 'px';
  9758. }
  9759. };
  9760. /**
  9761. * @constructor ItemRangeOverflow
  9762. * @extends ItemRange
  9763. * @param {ItemSet} parent
  9764. * @param {Object} data Object containing parameters start, end
  9765. * content, className.
  9766. * @param {Object} [options] Options to set initial property values
  9767. * @param {Object} [defaultOptions] default options
  9768. * // TODO: describe available options
  9769. */
  9770. function ItemRangeOverflow (parent, data, options, defaultOptions) {
  9771. this.props = {
  9772. content: {
  9773. left: 0,
  9774. width: 0
  9775. }
  9776. };
  9777. ItemRange.call(this, parent, data, options, defaultOptions);
  9778. }
  9779. ItemRangeOverflow.prototype = new ItemRange (null, null);
  9780. /**
  9781. * Repaint the item
  9782. * @return {Boolean} changed
  9783. */
  9784. ItemRangeOverflow.prototype.repaint = function repaint() {
  9785. // TODO: make an efficient repaint
  9786. var changed = false;
  9787. var dom = this.dom;
  9788. if (!dom) {
  9789. this._create();
  9790. dom = this.dom;
  9791. changed = true;
  9792. }
  9793. if (dom) {
  9794. if (!this.parent) {
  9795. throw new Error('Cannot repaint item: no parent attached');
  9796. }
  9797. var foreground = this.parent.getForeground();
  9798. if (!foreground) {
  9799. throw new Error('Cannot repaint time axis: ' +
  9800. 'parent has no foreground container element');
  9801. }
  9802. if (!dom.box.parentNode) {
  9803. foreground.appendChild(dom.box);
  9804. changed = true;
  9805. }
  9806. // update content
  9807. if (this.data.content != this.content) {
  9808. this.content = this.data.content;
  9809. if (this.content instanceof Element) {
  9810. dom.content.innerHTML = '';
  9811. dom.content.appendChild(this.content);
  9812. }
  9813. else if (this.data.content != undefined) {
  9814. dom.content.innerHTML = this.content;
  9815. }
  9816. else {
  9817. throw new Error('Property "content" missing in item ' + this.data.id);
  9818. }
  9819. changed = true;
  9820. }
  9821. // update class
  9822. var className = this.data.className ? (' ' + this.data.className) : '';
  9823. if (this.className != className) {
  9824. this.className = className;
  9825. dom.box.className = 'item rangeoverflow' + className;
  9826. changed = true;
  9827. }
  9828. }
  9829. return changed;
  9830. };
  9831. /**
  9832. * Return the items width
  9833. * @return {Number} width
  9834. */
  9835. ItemRangeOverflow.prototype.getWidth = function getWidth() {
  9836. if (this.props.content !== undefined && this.width < this.props.content.width)
  9837. return this.props.content.width;
  9838. else
  9839. return this.width;
  9840. };
  9841. /**
  9842. * @constructor Group
  9843. * @param {GroupSet} parent
  9844. * @param {Number | String} groupId
  9845. * @param {Object} [options] Options to set initial property values
  9846. * // TODO: describe available options
  9847. * @extends Component
  9848. */
  9849. function Group (parent, groupId, options) {
  9850. this.id = util.randomUUID();
  9851. this.parent = parent;
  9852. this.groupId = groupId;
  9853. this.itemset = null; // ItemSet
  9854. this.options = options || {};
  9855. this.options.top = 0;
  9856. this.props = {
  9857. label: {
  9858. width: 0,
  9859. height: 0
  9860. }
  9861. };
  9862. this.top = 0;
  9863. this.left = 0;
  9864. this.width = 0;
  9865. this.height = 0;
  9866. }
  9867. Group.prototype = new Component();
  9868. // TODO: comment
  9869. Group.prototype.setOptions = Component.prototype.setOptions;
  9870. /**
  9871. * Get the container element of the panel, which can be used by a child to
  9872. * add its own widgets.
  9873. * @returns {HTMLElement} container
  9874. */
  9875. Group.prototype.getContainer = function () {
  9876. return this.parent.getContainer();
  9877. };
  9878. /**
  9879. * Set item set for the group. The group will create a view on the itemset,
  9880. * filtered by the groups id.
  9881. * @param {DataSet | DataView} items
  9882. */
  9883. Group.prototype.setItems = function setItems(items) {
  9884. if (this.itemset) {
  9885. // remove current item set
  9886. this.itemset.hide();
  9887. this.itemset.setItems();
  9888. this.parent.controller.remove(this.itemset);
  9889. this.itemset = null;
  9890. }
  9891. if (items) {
  9892. var groupId = this.groupId;
  9893. var itemsetOptions = Object.create(this.options);
  9894. this.itemset = new ItemSet(this, null, itemsetOptions);
  9895. this.itemset.setRange(this.parent.range);
  9896. this.view = new DataView(items, {
  9897. filter: function (item) {
  9898. return item.group == groupId;
  9899. }
  9900. });
  9901. this.itemset.setItems(this.view);
  9902. this.parent.controller.add(this.itemset);
  9903. }
  9904. };
  9905. /**
  9906. * Repaint the item
  9907. * @return {Boolean} changed
  9908. */
  9909. Group.prototype.repaint = function repaint() {
  9910. return false;
  9911. };
  9912. /**
  9913. * Reflow the item
  9914. * @return {Boolean} resized
  9915. */
  9916. Group.prototype.reflow = function reflow() {
  9917. var changed = 0,
  9918. update = util.updateProperty;
  9919. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  9920. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  9921. // TODO: reckon with the height of the group label
  9922. if (this.label) {
  9923. var inner = this.label.firstChild;
  9924. changed += update(this.props.label, 'width', inner.clientWidth);
  9925. changed += update(this.props.label, 'height', inner.clientHeight);
  9926. }
  9927. else {
  9928. changed += update(this.props.label, 'width', 0);
  9929. changed += update(this.props.label, 'height', 0);
  9930. }
  9931. return (changed > 0);
  9932. };
  9933. /**
  9934. * An GroupSet holds a set of groups
  9935. * @param {Component} parent
  9936. * @param {Component[]} [depends] Components on which this components depends
  9937. * (except for the parent)
  9938. * @param {Object} [options] See GroupSet.setOptions for the available
  9939. * options.
  9940. * @constructor GroupSet
  9941. * @extends Panel
  9942. */
  9943. function GroupSet(parent, depends, options) {
  9944. this.id = util.randomUUID();
  9945. this.parent = parent;
  9946. this.depends = depends;
  9947. this.options = options || {};
  9948. this.range = null; // Range or Object {start: number, end: number}
  9949. this.itemsData = null; // DataSet with items
  9950. this.groupsData = null; // DataSet with groups
  9951. this.groups = {}; // map with groups
  9952. this.dom = {};
  9953. this.props = {
  9954. labels: {
  9955. width: 0
  9956. }
  9957. };
  9958. // TODO: implement right orientation of the labels
  9959. // changes in groups are queued key/value map containing id/action
  9960. this.queue = {};
  9961. var me = this;
  9962. this.listeners = {
  9963. 'add': function (event, params) {
  9964. me._onAdd(params.items);
  9965. },
  9966. 'update': function (event, params) {
  9967. me._onUpdate(params.items);
  9968. },
  9969. 'remove': function (event, params) {
  9970. me._onRemove(params.items);
  9971. }
  9972. };
  9973. }
  9974. GroupSet.prototype = new Panel();
  9975. /**
  9976. * Set options for the GroupSet. Existing options will be extended/overwritten.
  9977. * @param {Object} [options] The following options are available:
  9978. * {String | function} groupsOrder
  9979. * TODO: describe options
  9980. */
  9981. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  9982. GroupSet.prototype.setRange = function (range) {
  9983. // TODO: implement setRange
  9984. };
  9985. /**
  9986. * Set items
  9987. * @param {vis.DataSet | null} items
  9988. */
  9989. GroupSet.prototype.setItems = function setItems(items) {
  9990. this.itemsData = items;
  9991. for (var id in this.groups) {
  9992. if (this.groups.hasOwnProperty(id)) {
  9993. var group = this.groups[id];
  9994. group.setItems(items);
  9995. }
  9996. }
  9997. };
  9998. /**
  9999. * Get items
  10000. * @return {vis.DataSet | null} items
  10001. */
  10002. GroupSet.prototype.getItems = function getItems() {
  10003. return this.itemsData;
  10004. };
  10005. /**
  10006. * Set range (start and end).
  10007. * @param {Range | Object} range A Range or an object containing start and end.
  10008. */
  10009. GroupSet.prototype.setRange = function setRange(range) {
  10010. this.range = range;
  10011. };
  10012. /**
  10013. * Set groups
  10014. * @param {vis.DataSet} groups
  10015. */
  10016. GroupSet.prototype.setGroups = function setGroups(groups) {
  10017. var me = this,
  10018. ids;
  10019. // unsubscribe from current dataset
  10020. if (this.groupsData) {
  10021. util.forEach(this.listeners, function (callback, event) {
  10022. me.groupsData.unsubscribe(event, callback);
  10023. });
  10024. // remove all drawn groups
  10025. ids = this.groupsData.getIds();
  10026. this._onRemove(ids);
  10027. }
  10028. // replace the dataset
  10029. if (!groups) {
  10030. this.groupsData = null;
  10031. }
  10032. else if (groups instanceof DataSet) {
  10033. this.groupsData = groups;
  10034. }
  10035. else {
  10036. this.groupsData = new DataSet({
  10037. convert: {
  10038. start: 'Date',
  10039. end: 'Date'
  10040. }
  10041. });
  10042. this.groupsData.add(groups);
  10043. }
  10044. if (this.groupsData) {
  10045. // subscribe to new dataset
  10046. var id = this.id;
  10047. util.forEach(this.listeners, function (callback, event) {
  10048. me.groupsData.subscribe(event, callback, id);
  10049. });
  10050. // draw all new groups
  10051. ids = this.groupsData.getIds();
  10052. this._onAdd(ids);
  10053. }
  10054. };
  10055. /**
  10056. * Get groups
  10057. * @return {vis.DataSet | null} groups
  10058. */
  10059. GroupSet.prototype.getGroups = function getGroups() {
  10060. return this.groupsData;
  10061. };
  10062. /**
  10063. * Repaint the component
  10064. * @return {Boolean} changed
  10065. */
  10066. GroupSet.prototype.repaint = function repaint() {
  10067. var changed = 0,
  10068. i, id, group, label,
  10069. update = util.updateProperty,
  10070. asSize = util.option.asSize,
  10071. asElement = util.option.asElement,
  10072. options = this.options,
  10073. frame = this.dom.frame,
  10074. labels = this.dom.labels,
  10075. labelSet = this.dom.labelSet;
  10076. // create frame
  10077. if (!this.parent) {
  10078. throw new Error('Cannot repaint groupset: no parent attached');
  10079. }
  10080. var parentContainer = this.parent.getContainer();
  10081. if (!parentContainer) {
  10082. throw new Error('Cannot repaint groupset: parent has no container element');
  10083. }
  10084. if (!frame) {
  10085. frame = document.createElement('div');
  10086. frame.className = 'groupset';
  10087. this.dom.frame = frame;
  10088. var className = options.className;
  10089. if (className) {
  10090. util.addClassName(frame, util.option.asString(className));
  10091. }
  10092. changed += 1;
  10093. }
  10094. if (!frame.parentNode) {
  10095. parentContainer.appendChild(frame);
  10096. changed += 1;
  10097. }
  10098. // create labels
  10099. var labelContainer = asElement(options.labelContainer);
  10100. if (!labelContainer) {
  10101. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  10102. }
  10103. if (!labels) {
  10104. labels = document.createElement('div');
  10105. labels.className = 'labels';
  10106. this.dom.labels = labels;
  10107. }
  10108. if (!labelSet) {
  10109. labelSet = document.createElement('div');
  10110. labelSet.className = 'label-set';
  10111. labels.appendChild(labelSet);
  10112. this.dom.labelSet = labelSet;
  10113. }
  10114. if (!labels.parentNode || labels.parentNode != labelContainer) {
  10115. if (labels.parentNode) {
  10116. labels.parentNode.removeChild(labels.parentNode);
  10117. }
  10118. labelContainer.appendChild(labels);
  10119. }
  10120. // reposition frame
  10121. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  10122. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  10123. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  10124. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  10125. // reposition labels
  10126. changed += update(labelSet.style, 'top', asSize(options.top, '0px'));
  10127. changed += update(labelSet.style, 'height', asSize(options.height, this.height + 'px'));
  10128. var me = this,
  10129. queue = this.queue,
  10130. groups = this.groups,
  10131. groupsData = this.groupsData;
  10132. // show/hide added/changed/removed groups
  10133. var ids = Object.keys(queue);
  10134. if (ids.length) {
  10135. ids.forEach(function (id) {
  10136. var action = queue[id];
  10137. var group = groups[id];
  10138. //noinspection FallthroughInSwitchStatementJS
  10139. switch (action) {
  10140. case 'add':
  10141. case 'update':
  10142. if (!group) {
  10143. var groupOptions = Object.create(me.options);
  10144. util.extend(groupOptions, {
  10145. height: null,
  10146. maxHeight: null
  10147. });
  10148. group = new Group(me, id, groupOptions);
  10149. group.setItems(me.itemsData); // attach items data
  10150. groups[id] = group;
  10151. me.controller.add(group);
  10152. }
  10153. // TODO: update group data
  10154. group.data = groupsData.get(id);
  10155. delete queue[id];
  10156. break;
  10157. case 'remove':
  10158. if (group) {
  10159. group.setItems(); // detach items data
  10160. delete groups[id];
  10161. me.controller.remove(group);
  10162. }
  10163. // update lists
  10164. delete queue[id];
  10165. break;
  10166. default:
  10167. console.log('Error: unknown action "' + action + '"');
  10168. }
  10169. });
  10170. // the groupset depends on each of the groups
  10171. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  10172. // TODO: apply dependencies of the groupset
  10173. // update the top positions of the groups in the correct order
  10174. var orderedGroups = this.groupsData.getIds({
  10175. order: this.options.groupOrder
  10176. });
  10177. for (i = 0; i < orderedGroups.length; i++) {
  10178. (function (group, prevGroup) {
  10179. var top = 0;
  10180. if (prevGroup) {
  10181. top = function () {
  10182. // TODO: top must reckon with options.maxHeight
  10183. return prevGroup.top + prevGroup.height;
  10184. }
  10185. }
  10186. group.setOptions({
  10187. top: top
  10188. });
  10189. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  10190. }
  10191. // (re)create the labels
  10192. while (labelSet.firstChild) {
  10193. labelSet.removeChild(labelSet.firstChild);
  10194. }
  10195. for (i = 0; i < orderedGroups.length; i++) {
  10196. id = orderedGroups[i];
  10197. label = this._createLabel(id);
  10198. labelSet.appendChild(label);
  10199. }
  10200. changed++;
  10201. }
  10202. // reposition the labels
  10203. // TODO: labels are not displayed correctly when orientation=='top'
  10204. // TODO: width of labelPanel is not immediately updated on a change in groups
  10205. for (id in groups) {
  10206. if (groups.hasOwnProperty(id)) {
  10207. group = groups[id];
  10208. label = group.label;
  10209. if (label) {
  10210. label.style.top = group.top + 'px';
  10211. label.style.height = group.height + 'px';
  10212. }
  10213. }
  10214. }
  10215. return (changed > 0);
  10216. };
  10217. /**
  10218. * Create a label for group with given id
  10219. * @param {Number} id
  10220. * @return {Element} label
  10221. * @private
  10222. */
  10223. GroupSet.prototype._createLabel = function(id) {
  10224. var group = this.groups[id];
  10225. var label = document.createElement('div');
  10226. label.className = 'label';
  10227. var inner = document.createElement('div');
  10228. inner.className = 'inner';
  10229. label.appendChild(inner);
  10230. var content = group.data && group.data.content;
  10231. if (content instanceof Element) {
  10232. inner.appendChild(content);
  10233. }
  10234. else if (content != undefined) {
  10235. inner.innerHTML = content;
  10236. }
  10237. var className = group.data && group.data.className;
  10238. if (className) {
  10239. util.addClassName(label, className);
  10240. }
  10241. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  10242. return label;
  10243. };
  10244. /**
  10245. * Get container element
  10246. * @return {HTMLElement} container
  10247. */
  10248. GroupSet.prototype.getContainer = function getContainer() {
  10249. return this.dom.frame;
  10250. };
  10251. /**
  10252. * Get the width of the group labels
  10253. * @return {Number} width
  10254. */
  10255. GroupSet.prototype.getLabelsWidth = function getContainer() {
  10256. return this.props.labels.width;
  10257. };
  10258. /**
  10259. * Reflow the component
  10260. * @return {Boolean} resized
  10261. */
  10262. GroupSet.prototype.reflow = function reflow() {
  10263. var changed = 0,
  10264. id, group,
  10265. options = this.options,
  10266. update = util.updateProperty,
  10267. asNumber = util.option.asNumber,
  10268. asSize = util.option.asSize,
  10269. frame = this.dom.frame;
  10270. if (frame) {
  10271. var maxHeight = asNumber(options.maxHeight);
  10272. var fixedHeight = (asSize(options.height) != null);
  10273. var height;
  10274. if (fixedHeight) {
  10275. height = frame.offsetHeight;
  10276. }
  10277. else {
  10278. // height is not specified, calculate the sum of the height of all groups
  10279. height = 0;
  10280. for (id in this.groups) {
  10281. if (this.groups.hasOwnProperty(id)) {
  10282. group = this.groups[id];
  10283. height += group.height;
  10284. }
  10285. }
  10286. }
  10287. if (maxHeight != null) {
  10288. height = Math.min(height, maxHeight);
  10289. }
  10290. changed += update(this, 'height', height);
  10291. changed += update(this, 'top', frame.offsetTop);
  10292. changed += update(this, 'left', frame.offsetLeft);
  10293. changed += update(this, 'width', frame.offsetWidth);
  10294. }
  10295. // calculate the maximum width of the labels
  10296. var width = 0;
  10297. for (id in this.groups) {
  10298. if (this.groups.hasOwnProperty(id)) {
  10299. group = this.groups[id];
  10300. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  10301. width = Math.max(width, labelWidth);
  10302. }
  10303. }
  10304. changed += update(this.props.labels, 'width', width);
  10305. return (changed > 0);
  10306. };
  10307. /**
  10308. * Hide the component from the DOM
  10309. * @return {Boolean} changed
  10310. */
  10311. GroupSet.prototype.hide = function hide() {
  10312. if (this.dom.frame && this.dom.frame.parentNode) {
  10313. this.dom.frame.parentNode.removeChild(this.dom.frame);
  10314. return true;
  10315. }
  10316. else {
  10317. return false;
  10318. }
  10319. };
  10320. /**
  10321. * Show the component in the DOM (when not already visible).
  10322. * A repaint will be executed when the component is not visible
  10323. * @return {Boolean} changed
  10324. */
  10325. GroupSet.prototype.show = function show() {
  10326. if (!this.dom.frame || !this.dom.frame.parentNode) {
  10327. return this.repaint();
  10328. }
  10329. else {
  10330. return false;
  10331. }
  10332. };
  10333. /**
  10334. * Handle updated groups
  10335. * @param {Number[]} ids
  10336. * @private
  10337. */
  10338. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  10339. this._toQueue(ids, 'update');
  10340. };
  10341. /**
  10342. * Handle changed groups
  10343. * @param {Number[]} ids
  10344. * @private
  10345. */
  10346. GroupSet.prototype._onAdd = function _onAdd(ids) {
  10347. this._toQueue(ids, 'add');
  10348. };
  10349. /**
  10350. * Handle removed groups
  10351. * @param {Number[]} ids
  10352. * @private
  10353. */
  10354. GroupSet.prototype._onRemove = function _onRemove(ids) {
  10355. this._toQueue(ids, 'remove');
  10356. };
  10357. /**
  10358. * Put groups in the queue to be added/updated/remove
  10359. * @param {Number[]} ids
  10360. * @param {String} action can be 'add', 'update', 'remove'
  10361. */
  10362. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  10363. var queue = this.queue;
  10364. ids.forEach(function (id) {
  10365. queue[id] = action;
  10366. });
  10367. if (this.controller) {
  10368. //this.requestReflow();
  10369. this.requestRepaint();
  10370. }
  10371. };
  10372. /**
  10373. * Create a timeline visualization
  10374. * @param {HTMLElement} container
  10375. * @param {vis.DataSet | Array | DataTable} [items]
  10376. * @param {Object} [options] See Timeline.setOptions for the available options.
  10377. * @constructor
  10378. */
  10379. function Timeline (container, items, options) {
  10380. var me = this;
  10381. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  10382. this.options = {
  10383. orientation: 'bottom',
  10384. min: null,
  10385. max: null,
  10386. zoomMin: 10, // milliseconds
  10387. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  10388. // moveable: true, // TODO: option moveable
  10389. // zoomable: true, // TODO: option zoomable
  10390. showMinorLabels: true,
  10391. showMajorLabels: true,
  10392. showCurrentTime: false,
  10393. showCustomTime: false,
  10394. autoResize: false
  10395. };
  10396. // controller
  10397. this.controller = new Controller();
  10398. // root panel
  10399. if (!container) {
  10400. throw new Error('No container element provided');
  10401. }
  10402. var rootOptions = Object.create(this.options);
  10403. rootOptions.height = function () {
  10404. // TODO: change to height
  10405. if (me.options.height) {
  10406. // fixed height
  10407. return me.options.height;
  10408. }
  10409. else {
  10410. // auto height
  10411. return (me.timeaxis.height + me.content.height) + 'px';
  10412. }
  10413. };
  10414. this.rootPanel = new RootPanel(container, rootOptions);
  10415. this.controller.add(this.rootPanel);
  10416. // item panel
  10417. var itemOptions = Object.create(this.options);
  10418. itemOptions.left = function () {
  10419. return me.labelPanel.width;
  10420. };
  10421. itemOptions.width = function () {
  10422. return me.rootPanel.width - me.labelPanel.width;
  10423. };
  10424. itemOptions.top = null;
  10425. itemOptions.height = null;
  10426. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  10427. this.controller.add(this.itemPanel);
  10428. // label panel
  10429. var labelOptions = Object.create(this.options);
  10430. labelOptions.top = null;
  10431. labelOptions.left = null;
  10432. labelOptions.height = null;
  10433. labelOptions.width = function () {
  10434. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  10435. return me.content.getLabelsWidth();
  10436. }
  10437. else {
  10438. return 0;
  10439. }
  10440. };
  10441. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  10442. this.controller.add(this.labelPanel);
  10443. // range
  10444. var rangeOptions = Object.create(this.options);
  10445. this.range = new Range(rangeOptions);
  10446. this.range.setRange(
  10447. now.clone().add('days', -3).valueOf(),
  10448. now.clone().add('days', 4).valueOf()
  10449. );
  10450. // TODO: reckon with options moveable and zoomable
  10451. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  10452. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  10453. this.range.on('rangechange', function () {
  10454. var force = true;
  10455. me.controller.requestReflow(force);
  10456. });
  10457. this.range.on('rangechanged', function () {
  10458. var force = true;
  10459. me.controller.requestReflow(force);
  10460. });
  10461. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  10462. // time axis
  10463. var timeaxisOptions = Object.create(rootOptions);
  10464. timeaxisOptions.range = this.range;
  10465. timeaxisOptions.left = null;
  10466. timeaxisOptions.top = null;
  10467. timeaxisOptions.width = '100%';
  10468. timeaxisOptions.height = null;
  10469. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  10470. this.timeaxis.setRange(this.range);
  10471. this.controller.add(this.timeaxis);
  10472. // current time bar
  10473. this.currenttime = new CurrentTime(this.timeaxis, [], rootOptions);
  10474. this.controller.add(this.currenttime);
  10475. // custom time bar
  10476. this.customtime = new CustomTime(this.timeaxis, [], rootOptions);
  10477. this.controller.add(this.customtime);
  10478. // create groupset
  10479. this.setGroups(null);
  10480. this.itemsData = null; // DataSet
  10481. this.groupsData = null; // DataSet
  10482. // apply options
  10483. if (options) {
  10484. this.setOptions(options);
  10485. }
  10486. // create itemset and groupset
  10487. if (items) {
  10488. this.setItems(items);
  10489. }
  10490. }
  10491. /**
  10492. * Set options
  10493. * @param {Object} options TODO: describe the available options
  10494. */
  10495. Timeline.prototype.setOptions = function (options) {
  10496. util.extend(this.options, options);
  10497. // force update of range
  10498. // options.start and options.end can be undefined
  10499. //this.range.setRange(options.start, options.end);
  10500. this.range.setRange();
  10501. this.controller.reflow();
  10502. this.controller.repaint();
  10503. };
  10504. /**
  10505. * Set a custom time bar
  10506. * @param {Date} time
  10507. */
  10508. Timeline.prototype.setCustomTime = function (time) {
  10509. this.customtime._setCustomTime(time);
  10510. };
  10511. /**
  10512. * Retrieve the current custom time.
  10513. * @return {Date} customTime
  10514. */
  10515. Timeline.prototype.getCustomTime = function() {
  10516. return new Date(this.customtime.customTime.valueOf());
  10517. };
  10518. /**
  10519. * Set items
  10520. * @param {vis.DataSet | Array | DataTable | null} items
  10521. */
  10522. Timeline.prototype.setItems = function(items) {
  10523. var initialLoad = (this.itemsData == null);
  10524. // convert to type DataSet when needed
  10525. var newItemSet;
  10526. if (!items) {
  10527. newItemSet = null;
  10528. }
  10529. else if (items instanceof DataSet) {
  10530. newItemSet = items;
  10531. }
  10532. if (!(items instanceof DataSet)) {
  10533. newItemSet = new DataSet({
  10534. convert: {
  10535. start: 'Date',
  10536. end: 'Date'
  10537. }
  10538. });
  10539. newItemSet.add(items);
  10540. }
  10541. // set items
  10542. this.itemsData = newItemSet;
  10543. this.content.setItems(newItemSet);
  10544. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  10545. // apply the data range as range
  10546. var dataRange = this.getItemRange();
  10547. // add 5% space on both sides
  10548. var min = dataRange.min;
  10549. var max = dataRange.max;
  10550. if (min != null && max != null) {
  10551. var interval = (max.valueOf() - min.valueOf());
  10552. if (interval <= 0) {
  10553. // prevent an empty interval
  10554. interval = 24 * 60 * 60 * 1000; // 1 day
  10555. }
  10556. min = new Date(min.valueOf() - interval * 0.05);
  10557. max = new Date(max.valueOf() + interval * 0.05);
  10558. }
  10559. // override specified start and/or end date
  10560. if (this.options.start != undefined) {
  10561. min = util.convert(this.options.start, 'Date');
  10562. }
  10563. if (this.options.end != undefined) {
  10564. max = util.convert(this.options.end, 'Date');
  10565. }
  10566. // apply range if there is a min or max available
  10567. if (min != null || max != null) {
  10568. this.range.setRange(min, max);
  10569. }
  10570. }
  10571. };
  10572. /**
  10573. * Set groups
  10574. * @param {vis.DataSet | Array | DataTable} groups
  10575. */
  10576. Timeline.prototype.setGroups = function(groups) {
  10577. var me = this;
  10578. this.groupsData = groups;
  10579. // switch content type between ItemSet or GroupSet when needed
  10580. var Type = this.groupsData ? GroupSet : ItemSet;
  10581. if (!(this.content instanceof Type)) {
  10582. // remove old content set
  10583. if (this.content) {
  10584. this.content.hide();
  10585. if (this.content.setItems) {
  10586. this.content.setItems(); // disconnect from items
  10587. }
  10588. if (this.content.setGroups) {
  10589. this.content.setGroups(); // disconnect from groups
  10590. }
  10591. this.controller.remove(this.content);
  10592. }
  10593. // create new content set
  10594. var options = Object.create(this.options);
  10595. util.extend(options, {
  10596. top: function () {
  10597. if (me.options.orientation == 'top') {
  10598. return me.timeaxis.height;
  10599. }
  10600. else {
  10601. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  10602. }
  10603. },
  10604. left: null,
  10605. width: '100%',
  10606. height: function () {
  10607. if (me.options.height) {
  10608. // fixed height
  10609. return me.itemPanel.height - me.timeaxis.height;
  10610. }
  10611. else {
  10612. // auto height
  10613. return null;
  10614. }
  10615. },
  10616. maxHeight: function () {
  10617. // TODO: change maxHeight to be a css string like '100%' or '300px'
  10618. if (me.options.maxHeight) {
  10619. if (!util.isNumber(me.options.maxHeight)) {
  10620. throw new TypeError('Number expected for property maxHeight');
  10621. }
  10622. return me.options.maxHeight - me.timeaxis.height;
  10623. }
  10624. else {
  10625. return null;
  10626. }
  10627. },
  10628. labelContainer: function () {
  10629. return me.labelPanel.getContainer();
  10630. }
  10631. });
  10632. this.content = new Type(this.itemPanel, [this.timeaxis], options);
  10633. if (this.content.setRange) {
  10634. this.content.setRange(this.range);
  10635. }
  10636. if (this.content.setItems) {
  10637. this.content.setItems(this.itemsData);
  10638. }
  10639. if (this.content.setGroups) {
  10640. this.content.setGroups(this.groupsData);
  10641. }
  10642. this.controller.add(this.content);
  10643. }
  10644. };
  10645. /**
  10646. * Get the data range of the item set.
  10647. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  10648. * When no minimum is found, min==null
  10649. * When no maximum is found, max==null
  10650. */
  10651. Timeline.prototype.getItemRange = function getItemRange() {
  10652. // calculate min from start filed
  10653. var itemsData = this.itemsData,
  10654. min = null,
  10655. max = null;
  10656. if (itemsData) {
  10657. // calculate the minimum value of the field 'start'
  10658. var minItem = itemsData.min('start');
  10659. min = minItem ? minItem.start.valueOf() : null;
  10660. // calculate maximum value of fields 'start' and 'end'
  10661. var maxStartItem = itemsData.max('start');
  10662. if (maxStartItem) {
  10663. max = maxStartItem.start.valueOf();
  10664. }
  10665. var maxEndItem = itemsData.max('end');
  10666. if (maxEndItem) {
  10667. if (max == null) {
  10668. max = maxEndItem.end.valueOf();
  10669. }
  10670. else {
  10671. max = Math.max(max, maxEndItem.end.valueOf());
  10672. }
  10673. }
  10674. }
  10675. return {
  10676. min: (min != null) ? new Date(min) : null,
  10677. max: (max != null) ? new Date(max) : null
  10678. };
  10679. };
  10680. (function(exports) {
  10681. /**
  10682. * Parse a text source containing data in DOT language into a JSON object.
  10683. * The object contains two lists: one with nodes and one with edges.
  10684. *
  10685. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  10686. *
  10687. * @param {String} data Text containing a graph in DOT-notation
  10688. * @return {Object} graph An object containing two parameters:
  10689. * {Object[]} nodes
  10690. * {Object[]} edges
  10691. */
  10692. function parseDOT (data) {
  10693. dot = data;
  10694. return parseGraph();
  10695. }
  10696. // token types enumeration
  10697. var TOKENTYPE = {
  10698. NULL : 0,
  10699. DELIMITER : 1,
  10700. IDENTIFIER: 2,
  10701. UNKNOWN : 3
  10702. };
  10703. // map with all delimiters
  10704. var DELIMITERS = {
  10705. '{': true,
  10706. '}': true,
  10707. '[': true,
  10708. ']': true,
  10709. ';': true,
  10710. '=': true,
  10711. ',': true,
  10712. '->': true,
  10713. '--': true
  10714. };
  10715. var dot = ''; // current dot file
  10716. var index = 0; // current index in dot file
  10717. var c = ''; // current token character in expr
  10718. var token = ''; // current token
  10719. var tokenType = TOKENTYPE.NULL; // type of the token
  10720. /**
  10721. * Get the first character from the dot file.
  10722. * The character is stored into the char c. If the end of the dot file is
  10723. * reached, the function puts an empty string in c.
  10724. */
  10725. function first() {
  10726. index = 0;
  10727. c = dot.charAt(0);
  10728. }
  10729. /**
  10730. * Get the next character from the dot file.
  10731. * The character is stored into the char c. If the end of the dot file is
  10732. * reached, the function puts an empty string in c.
  10733. */
  10734. function next() {
  10735. index++;
  10736. c = dot.charAt(index);
  10737. }
  10738. /**
  10739. * Preview the next character from the dot file.
  10740. * @return {String} cNext
  10741. */
  10742. function nextPreview() {
  10743. return dot.charAt(index + 1);
  10744. }
  10745. /**
  10746. * Test whether given character is alphabetic or numeric
  10747. * @param {String} c
  10748. * @return {Boolean} isAlphaNumeric
  10749. */
  10750. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  10751. function isAlphaNumeric(c) {
  10752. return regexAlphaNumeric.test(c);
  10753. }
  10754. /**
  10755. * Merge all properties of object b into object b
  10756. * @param {Object} a
  10757. * @param {Object} b
  10758. * @return {Object} a
  10759. */
  10760. function merge (a, b) {
  10761. if (!a) {
  10762. a = {};
  10763. }
  10764. if (b) {
  10765. for (var name in b) {
  10766. if (b.hasOwnProperty(name)) {
  10767. a[name] = b[name];
  10768. }
  10769. }
  10770. }
  10771. return a;
  10772. }
  10773. /**
  10774. * Set a value in an object, where the provided parameter name can be a
  10775. * path with nested parameters. For example:
  10776. *
  10777. * var obj = {a: 2};
  10778. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  10779. *
  10780. * @param {Object} obj
  10781. * @param {String} path A parameter name or dot-separated parameter path,
  10782. * like "color.highlight.border".
  10783. * @param {*} value
  10784. */
  10785. function setValue(obj, path, value) {
  10786. var keys = path.split('.');
  10787. var o = obj;
  10788. while (keys.length) {
  10789. var key = keys.shift();
  10790. if (keys.length) {
  10791. // this isn't the end point
  10792. if (!o[key]) {
  10793. o[key] = {};
  10794. }
  10795. o = o[key];
  10796. }
  10797. else {
  10798. // this is the end point
  10799. o[key] = value;
  10800. }
  10801. }
  10802. }
  10803. /**
  10804. * Add a node to a graph object. If there is already a node with
  10805. * the same id, their attributes will be merged.
  10806. * @param {Object} graph
  10807. * @param {Object} node
  10808. */
  10809. function addNode(graph, node) {
  10810. var i, len;
  10811. var current = null;
  10812. // find root graph (in case of subgraph)
  10813. var graphs = [graph]; // list with all graphs from current graph to root graph
  10814. var root = graph;
  10815. while (root.parent) {
  10816. graphs.push(root.parent);
  10817. root = root.parent;
  10818. }
  10819. // find existing node (at root level) by its id
  10820. if (root.nodes) {
  10821. for (i = 0, len = root.nodes.length; i < len; i++) {
  10822. if (node.id === root.nodes[i].id) {
  10823. current = root.nodes[i];
  10824. break;
  10825. }
  10826. }
  10827. }
  10828. if (!current) {
  10829. // this is a new node
  10830. current = {
  10831. id: node.id
  10832. };
  10833. if (graph.node) {
  10834. // clone default attributes
  10835. current.attr = merge(current.attr, graph.node);
  10836. }
  10837. }
  10838. // add node to this (sub)graph and all its parent graphs
  10839. for (i = graphs.length - 1; i >= 0; i--) {
  10840. var g = graphs[i];
  10841. if (!g.nodes) {
  10842. g.nodes = [];
  10843. }
  10844. if (g.nodes.indexOf(current) == -1) {
  10845. g.nodes.push(current);
  10846. }
  10847. }
  10848. // merge attributes
  10849. if (node.attr) {
  10850. current.attr = merge(current.attr, node.attr);
  10851. }
  10852. }
  10853. /**
  10854. * Add an edge to a graph object
  10855. * @param {Object} graph
  10856. * @param {Object} edge
  10857. */
  10858. function addEdge(graph, edge) {
  10859. if (!graph.edges) {
  10860. graph.edges = [];
  10861. }
  10862. graph.edges.push(edge);
  10863. if (graph.edge) {
  10864. var attr = merge({}, graph.edge); // clone default attributes
  10865. edge.attr = merge(attr, edge.attr); // merge attributes
  10866. }
  10867. }
  10868. /**
  10869. * Create an edge to a graph object
  10870. * @param {Object} graph
  10871. * @param {String | Number | Object} from
  10872. * @param {String | Number | Object} to
  10873. * @param {String} type
  10874. * @param {Object | null} attr
  10875. * @return {Object} edge
  10876. */
  10877. function createEdge(graph, from, to, type, attr) {
  10878. var edge = {
  10879. from: from,
  10880. to: to,
  10881. type: type
  10882. };
  10883. if (graph.edge) {
  10884. edge.attr = merge({}, graph.edge); // clone default attributes
  10885. }
  10886. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  10887. return edge;
  10888. }
  10889. /**
  10890. * Get next token in the current dot file.
  10891. * The token and token type are available as token and tokenType
  10892. */
  10893. function getToken() {
  10894. tokenType = TOKENTYPE.NULL;
  10895. token = '';
  10896. // skip over whitespaces
  10897. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10898. next();
  10899. }
  10900. do {
  10901. var isComment = false;
  10902. // skip comment
  10903. if (c == '#') {
  10904. // find the previous non-space character
  10905. var i = index - 1;
  10906. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  10907. i--;
  10908. }
  10909. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  10910. // the # is at the start of a line, this is indeed a line comment
  10911. while (c != '' && c != '\n') {
  10912. next();
  10913. }
  10914. isComment = true;
  10915. }
  10916. }
  10917. if (c == '/' && nextPreview() == '/') {
  10918. // skip line comment
  10919. while (c != '' && c != '\n') {
  10920. next();
  10921. }
  10922. isComment = true;
  10923. }
  10924. if (c == '/' && nextPreview() == '*') {
  10925. // skip block comment
  10926. while (c != '') {
  10927. if (c == '*' && nextPreview() == '/') {
  10928. // end of block comment found. skip these last two characters
  10929. next();
  10930. next();
  10931. break;
  10932. }
  10933. else {
  10934. next();
  10935. }
  10936. }
  10937. isComment = true;
  10938. }
  10939. // skip over whitespaces
  10940. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  10941. next();
  10942. }
  10943. }
  10944. while (isComment);
  10945. // check for end of dot file
  10946. if (c == '') {
  10947. // token is still empty
  10948. tokenType = TOKENTYPE.DELIMITER;
  10949. return;
  10950. }
  10951. // check for delimiters consisting of 2 characters
  10952. var c2 = c + nextPreview();
  10953. if (DELIMITERS[c2]) {
  10954. tokenType = TOKENTYPE.DELIMITER;
  10955. token = c2;
  10956. next();
  10957. next();
  10958. return;
  10959. }
  10960. // check for delimiters consisting of 1 character
  10961. if (DELIMITERS[c]) {
  10962. tokenType = TOKENTYPE.DELIMITER;
  10963. token = c;
  10964. next();
  10965. return;
  10966. }
  10967. // check for an identifier (number or string)
  10968. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  10969. if (isAlphaNumeric(c) || c == '-') {
  10970. token += c;
  10971. next();
  10972. while (isAlphaNumeric(c)) {
  10973. token += c;
  10974. next();
  10975. }
  10976. if (token == 'false') {
  10977. token = false; // convert to boolean
  10978. }
  10979. else if (token == 'true') {
  10980. token = true; // convert to boolean
  10981. }
  10982. else if (!isNaN(Number(token))) {
  10983. token = Number(token); // convert to number
  10984. }
  10985. tokenType = TOKENTYPE.IDENTIFIER;
  10986. return;
  10987. }
  10988. // check for a string enclosed by double quotes
  10989. if (c == '"') {
  10990. next();
  10991. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  10992. token += c;
  10993. if (c == '"') { // skip the escape character
  10994. next();
  10995. }
  10996. next();
  10997. }
  10998. if (c != '"') {
  10999. throw newSyntaxError('End of string " expected');
  11000. }
  11001. next();
  11002. tokenType = TOKENTYPE.IDENTIFIER;
  11003. return;
  11004. }
  11005. // something unknown is found, wrong characters, a syntax error
  11006. tokenType = TOKENTYPE.UNKNOWN;
  11007. while (c != '') {
  11008. token += c;
  11009. next();
  11010. }
  11011. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  11012. }
  11013. /**
  11014. * Parse a graph.
  11015. * @returns {Object} graph
  11016. */
  11017. function parseGraph() {
  11018. var graph = {};
  11019. first();
  11020. getToken();
  11021. // optional strict keyword
  11022. if (token == 'strict') {
  11023. graph.strict = true;
  11024. getToken();
  11025. }
  11026. // graph or digraph keyword
  11027. if (token == 'graph' || token == 'digraph') {
  11028. graph.type = token;
  11029. getToken();
  11030. }
  11031. // optional graph id
  11032. if (tokenType == TOKENTYPE.IDENTIFIER) {
  11033. graph.id = token;
  11034. getToken();
  11035. }
  11036. // open angle bracket
  11037. if (token != '{') {
  11038. throw newSyntaxError('Angle bracket { expected');
  11039. }
  11040. getToken();
  11041. // statements
  11042. parseStatements(graph);
  11043. // close angle bracket
  11044. if (token != '}') {
  11045. throw newSyntaxError('Angle bracket } expected');
  11046. }
  11047. getToken();
  11048. // end of file
  11049. if (token !== '') {
  11050. throw newSyntaxError('End of file expected');
  11051. }
  11052. getToken();
  11053. // remove temporary default properties
  11054. delete graph.node;
  11055. delete graph.edge;
  11056. delete graph.graph;
  11057. return graph;
  11058. }
  11059. /**
  11060. * Parse a list with statements.
  11061. * @param {Object} graph
  11062. */
  11063. function parseStatements (graph) {
  11064. while (token !== '' && token != '}') {
  11065. parseStatement(graph);
  11066. if (token == ';') {
  11067. getToken();
  11068. }
  11069. }
  11070. }
  11071. /**
  11072. * Parse a single statement. Can be a an attribute statement, node
  11073. * statement, a series of node statements and edge statements, or a
  11074. * parameter.
  11075. * @param {Object} graph
  11076. */
  11077. function parseStatement(graph) {
  11078. // parse subgraph
  11079. var subgraph = parseSubgraph(graph);
  11080. if (subgraph) {
  11081. // edge statements
  11082. parseEdge(graph, subgraph);
  11083. return;
  11084. }
  11085. // parse an attribute statement
  11086. var attr = parseAttributeStatement(graph);
  11087. if (attr) {
  11088. return;
  11089. }
  11090. // parse node
  11091. if (tokenType != TOKENTYPE.IDENTIFIER) {
  11092. throw newSyntaxError('Identifier expected');
  11093. }
  11094. var id = token; // id can be a string or a number
  11095. getToken();
  11096. if (token == '=') {
  11097. // id statement
  11098. getToken();
  11099. if (tokenType != TOKENTYPE.IDENTIFIER) {
  11100. throw newSyntaxError('Identifier expected');
  11101. }
  11102. graph[id] = token;
  11103. getToken();
  11104. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  11105. }
  11106. else {
  11107. parseNodeStatement(graph, id);
  11108. }
  11109. }
  11110. /**
  11111. * Parse a subgraph
  11112. * @param {Object} graph parent graph object
  11113. * @return {Object | null} subgraph
  11114. */
  11115. function parseSubgraph (graph) {
  11116. var subgraph = null;
  11117. // optional subgraph keyword
  11118. if (token == 'subgraph') {
  11119. subgraph = {};
  11120. subgraph.type = 'subgraph';
  11121. getToken();
  11122. // optional graph id
  11123. if (tokenType == TOKENTYPE.IDENTIFIER) {
  11124. subgraph.id = token;
  11125. getToken();
  11126. }
  11127. }
  11128. // open angle bracket
  11129. if (token == '{') {
  11130. getToken();
  11131. if (!subgraph) {
  11132. subgraph = {};
  11133. }
  11134. subgraph.parent = graph;
  11135. subgraph.node = graph.node;
  11136. subgraph.edge = graph.edge;
  11137. subgraph.graph = graph.graph;
  11138. // statements
  11139. parseStatements(subgraph);
  11140. // close angle bracket
  11141. if (token != '}') {
  11142. throw newSyntaxError('Angle bracket } expected');
  11143. }
  11144. getToken();
  11145. // remove temporary default properties
  11146. delete subgraph.node;
  11147. delete subgraph.edge;
  11148. delete subgraph.graph;
  11149. delete subgraph.parent;
  11150. // register at the parent graph
  11151. if (!graph.subgraphs) {
  11152. graph.subgraphs = [];
  11153. }
  11154. graph.subgraphs.push(subgraph);
  11155. }
  11156. return subgraph;
  11157. }
  11158. /**
  11159. * parse an attribute statement like "node [shape=circle fontSize=16]".
  11160. * Available keywords are 'node', 'edge', 'graph'.
  11161. * The previous list with default attributes will be replaced
  11162. * @param {Object} graph
  11163. * @returns {String | null} keyword Returns the name of the parsed attribute
  11164. * (node, edge, graph), or null if nothing
  11165. * is parsed.
  11166. */
  11167. function parseAttributeStatement (graph) {
  11168. // attribute statements
  11169. if (token == 'node') {
  11170. getToken();
  11171. // node attributes
  11172. graph.node = parseAttributeList();
  11173. return 'node';
  11174. }
  11175. else if (token == 'edge') {
  11176. getToken();
  11177. // edge attributes
  11178. graph.edge = parseAttributeList();
  11179. return 'edge';
  11180. }
  11181. else if (token == 'graph') {
  11182. getToken();
  11183. // graph attributes
  11184. graph.graph = parseAttributeList();
  11185. return 'graph';
  11186. }
  11187. return null;
  11188. }
  11189. /**
  11190. * parse a node statement
  11191. * @param {Object} graph
  11192. * @param {String | Number} id
  11193. */
  11194. function parseNodeStatement(graph, id) {
  11195. // node statement
  11196. var node = {
  11197. id: id
  11198. };
  11199. var attr = parseAttributeList();
  11200. if (attr) {
  11201. node.attr = attr;
  11202. }
  11203. addNode(graph, node);
  11204. // edge statements
  11205. parseEdge(graph, id);
  11206. }
  11207. /**
  11208. * Parse an edge or a series of edges
  11209. * @param {Object} graph
  11210. * @param {String | Number} from Id of the from node
  11211. */
  11212. function parseEdge(graph, from) {
  11213. while (token == '->' || token == '--') {
  11214. var to;
  11215. var type = token;
  11216. getToken();
  11217. var subgraph = parseSubgraph(graph);
  11218. if (subgraph) {
  11219. to = subgraph;
  11220. }
  11221. else {
  11222. if (tokenType != TOKENTYPE.IDENTIFIER) {
  11223. throw newSyntaxError('Identifier or subgraph expected');
  11224. }
  11225. to = token;
  11226. addNode(graph, {
  11227. id: to
  11228. });
  11229. getToken();
  11230. }
  11231. // parse edge attributes
  11232. var attr = parseAttributeList();
  11233. // create edge
  11234. var edge = createEdge(graph, from, to, type, attr);
  11235. addEdge(graph, edge);
  11236. from = to;
  11237. }
  11238. }
  11239. /**
  11240. * Parse a set with attributes,
  11241. * for example [label="1.000", shape=solid]
  11242. * @return {Object | null} attr
  11243. */
  11244. function parseAttributeList() {
  11245. var attr = null;
  11246. while (token == '[') {
  11247. getToken();
  11248. attr = {};
  11249. while (token !== '' && token != ']') {
  11250. if (tokenType != TOKENTYPE.IDENTIFIER) {
  11251. throw newSyntaxError('Attribute name expected');
  11252. }
  11253. var name = token;
  11254. getToken();
  11255. if (token != '=') {
  11256. throw newSyntaxError('Equal sign = expected');
  11257. }
  11258. getToken();
  11259. if (tokenType != TOKENTYPE.IDENTIFIER) {
  11260. throw newSyntaxError('Attribute value expected');
  11261. }
  11262. var value = token;
  11263. setValue(attr, name, value); // name can be a path
  11264. getToken();
  11265. if (token ==',') {
  11266. getToken();
  11267. }
  11268. }
  11269. if (token != ']') {
  11270. throw newSyntaxError('Bracket ] expected');
  11271. }
  11272. getToken();
  11273. }
  11274. return attr;
  11275. }
  11276. /**
  11277. * Create a syntax error with extra information on current token and index.
  11278. * @param {String} message
  11279. * @returns {SyntaxError} err
  11280. */
  11281. function newSyntaxError(message) {
  11282. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  11283. }
  11284. /**
  11285. * Chop off text after a maximum length
  11286. * @param {String} text
  11287. * @param {Number} maxLength
  11288. * @returns {String}
  11289. */
  11290. function chop (text, maxLength) {
  11291. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  11292. }
  11293. /**
  11294. * Execute a function fn for each pair of elements in two arrays
  11295. * @param {Array | *} array1
  11296. * @param {Array | *} array2
  11297. * @param {function} fn
  11298. */
  11299. function forEach2(array1, array2, fn) {
  11300. if (array1 instanceof Array) {
  11301. array1.forEach(function (elem1) {
  11302. if (array2 instanceof Array) {
  11303. array2.forEach(function (elem2) {
  11304. fn(elem1, elem2);
  11305. });
  11306. }
  11307. else {
  11308. fn(elem1, array2);
  11309. }
  11310. });
  11311. }
  11312. else {
  11313. if (array2 instanceof Array) {
  11314. array2.forEach(function (elem2) {
  11315. fn(array1, elem2);
  11316. });
  11317. }
  11318. else {
  11319. fn(array1, array2);
  11320. }
  11321. }
  11322. }
  11323. /**
  11324. * Convert a string containing a graph in DOT language into a map containing
  11325. * with nodes and edges in the format of graph.
  11326. * @param {String} data Text containing a graph in DOT-notation
  11327. * @return {Object} graphData
  11328. */
  11329. function DOTToGraph (data) {
  11330. // parse the DOT file
  11331. var dotData = parseDOT(data);
  11332. var graphData = {
  11333. nodes: [],
  11334. edges: [],
  11335. options: {}
  11336. };
  11337. // copy the nodes
  11338. if (dotData.nodes) {
  11339. dotData.nodes.forEach(function (dotNode) {
  11340. var graphNode = {
  11341. id: dotNode.id,
  11342. label: String(dotNode.label || dotNode.id)
  11343. };
  11344. merge(graphNode, dotNode.attr);
  11345. if (graphNode.image) {
  11346. graphNode.shape = 'image';
  11347. }
  11348. graphData.nodes.push(graphNode);
  11349. });
  11350. }
  11351. // copy the edges
  11352. if (dotData.edges) {
  11353. /**
  11354. * Convert an edge in DOT format to an edge with VisGraph format
  11355. * @param {Object} dotEdge
  11356. * @returns {Object} graphEdge
  11357. */
  11358. function convertEdge(dotEdge) {
  11359. var graphEdge = {
  11360. from: dotEdge.from,
  11361. to: dotEdge.to
  11362. };
  11363. merge(graphEdge, dotEdge.attr);
  11364. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  11365. return graphEdge;
  11366. }
  11367. dotData.edges.forEach(function (dotEdge) {
  11368. var from, to;
  11369. if (dotEdge.from instanceof Object) {
  11370. from = dotEdge.from.nodes;
  11371. }
  11372. else {
  11373. from = {
  11374. id: dotEdge.from
  11375. }
  11376. }
  11377. if (dotEdge.to instanceof Object) {
  11378. to = dotEdge.to.nodes;
  11379. }
  11380. else {
  11381. to = {
  11382. id: dotEdge.to
  11383. }
  11384. }
  11385. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  11386. dotEdge.from.edges.forEach(function (subEdge) {
  11387. var graphEdge = convertEdge(subEdge);
  11388. graphData.edges.push(graphEdge);
  11389. });
  11390. }
  11391. forEach2(from, to, function (from, to) {
  11392. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  11393. var graphEdge = convertEdge(subEdge);
  11394. graphData.edges.push(graphEdge);
  11395. });
  11396. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  11397. dotEdge.to.edges.forEach(function (subEdge) {
  11398. var graphEdge = convertEdge(subEdge);
  11399. graphData.edges.push(graphEdge);
  11400. });
  11401. }
  11402. });
  11403. }
  11404. // copy the options
  11405. if (dotData.attr) {
  11406. graphData.options = dotData.attr;
  11407. }
  11408. return graphData;
  11409. }
  11410. // exports
  11411. exports.parseDOT = parseDOT;
  11412. exports.DOTToGraph = DOTToGraph;
  11413. })(typeof util !== 'undefined' ? util : exports);
  11414. /**
  11415. * Canvas shapes used by the Graph
  11416. */
  11417. if (typeof CanvasRenderingContext2D !== 'undefined') {
  11418. /**
  11419. * Draw a circle shape
  11420. */
  11421. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  11422. this.beginPath();
  11423. this.arc(x, y, r, 0, 2*Math.PI, false);
  11424. };
  11425. /**
  11426. * Draw a square shape
  11427. * @param {Number} x horizontal center
  11428. * @param {Number} y vertical center
  11429. * @param {Number} r size, width and height of the square
  11430. */
  11431. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  11432. this.beginPath();
  11433. this.rect(x - r, y - r, r * 2, r * 2);
  11434. };
  11435. /**
  11436. * Draw a triangle shape
  11437. * @param {Number} x horizontal center
  11438. * @param {Number} y vertical center
  11439. * @param {Number} r radius, half the length of the sides of the triangle
  11440. */
  11441. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  11442. // http://en.wikipedia.org/wiki/Equilateral_triangle
  11443. this.beginPath();
  11444. var s = r * 2;
  11445. var s2 = s / 2;
  11446. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  11447. var h = Math.sqrt(s * s - s2 * s2); // height
  11448. this.moveTo(x, y - (h - ir));
  11449. this.lineTo(x + s2, y + ir);
  11450. this.lineTo(x - s2, y + ir);
  11451. this.lineTo(x, y - (h - ir));
  11452. this.closePath();
  11453. };
  11454. /**
  11455. * Draw a triangle shape in downward orientation
  11456. * @param {Number} x horizontal center
  11457. * @param {Number} y vertical center
  11458. * @param {Number} r radius
  11459. */
  11460. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  11461. // http://en.wikipedia.org/wiki/Equilateral_triangle
  11462. this.beginPath();
  11463. var s = r * 2;
  11464. var s2 = s / 2;
  11465. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  11466. var h = Math.sqrt(s * s - s2 * s2); // height
  11467. this.moveTo(x, y + (h - ir));
  11468. this.lineTo(x + s2, y - ir);
  11469. this.lineTo(x - s2, y - ir);
  11470. this.lineTo(x, y + (h - ir));
  11471. this.closePath();
  11472. };
  11473. /**
  11474. * Draw a star shape, a star with 5 points
  11475. * @param {Number} x horizontal center
  11476. * @param {Number} y vertical center
  11477. * @param {Number} r radius, half the length of the sides of the triangle
  11478. */
  11479. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  11480. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  11481. this.beginPath();
  11482. for (var n = 0; n < 10; n++) {
  11483. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  11484. this.lineTo(
  11485. x + radius * Math.sin(n * 2 * Math.PI / 10),
  11486. y - radius * Math.cos(n * 2 * Math.PI / 10)
  11487. );
  11488. }
  11489. this.closePath();
  11490. };
  11491. /**
  11492. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  11493. */
  11494. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  11495. var r2d = Math.PI/180;
  11496. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  11497. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  11498. this.beginPath();
  11499. this.moveTo(x+r,y);
  11500. this.lineTo(x+w-r,y);
  11501. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  11502. this.lineTo(x+w,y+h-r);
  11503. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  11504. this.lineTo(x+r,y+h);
  11505. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  11506. this.lineTo(x,y+r);
  11507. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  11508. };
  11509. /**
  11510. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  11511. */
  11512. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  11513. var kappa = .5522848,
  11514. ox = (w / 2) * kappa, // control point offset horizontal
  11515. oy = (h / 2) * kappa, // control point offset vertical
  11516. xe = x + w, // x-end
  11517. ye = y + h, // y-end
  11518. xm = x + w / 2, // x-middle
  11519. ym = y + h / 2; // y-middle
  11520. this.beginPath();
  11521. this.moveTo(x, ym);
  11522. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  11523. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  11524. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  11525. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  11526. };
  11527. /**
  11528. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  11529. */
  11530. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  11531. var f = 1/3;
  11532. var wEllipse = w;
  11533. var hEllipse = h * f;
  11534. var kappa = .5522848,
  11535. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  11536. oy = (hEllipse / 2) * kappa, // control point offset vertical
  11537. xe = x + wEllipse, // x-end
  11538. ye = y + hEllipse, // y-end
  11539. xm = x + wEllipse / 2, // x-middle
  11540. ym = y + hEllipse / 2, // y-middle
  11541. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  11542. yeb = y + h; // y-end, bottom ellipse
  11543. this.beginPath();
  11544. this.moveTo(xe, ym);
  11545. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  11546. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  11547. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  11548. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  11549. this.lineTo(xe, ymb);
  11550. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  11551. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  11552. this.lineTo(x, ym);
  11553. };
  11554. /**
  11555. * Draw an arrow point (no line)
  11556. */
  11557. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  11558. // tail
  11559. var xt = x - length * Math.cos(angle);
  11560. var yt = y - length * Math.sin(angle);
  11561. // inner tail
  11562. // TODO: allow to customize different shapes
  11563. var xi = x - length * 0.9 * Math.cos(angle);
  11564. var yi = y - length * 0.9 * Math.sin(angle);
  11565. // left
  11566. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  11567. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  11568. // right
  11569. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  11570. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  11571. this.beginPath();
  11572. this.moveTo(x, y);
  11573. this.lineTo(xl, yl);
  11574. this.lineTo(xi, yi);
  11575. this.lineTo(xr, yr);
  11576. this.closePath();
  11577. };
  11578. /**
  11579. * Sets up the dashedLine functionality for drawing
  11580. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  11581. * @author David Jordan
  11582. * @date 2012-08-08
  11583. */
  11584. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  11585. if (!dashArray) dashArray=[10,5];
  11586. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  11587. var dashCount = dashArray.length;
  11588. this.moveTo(x, y);
  11589. var dx = (x2-x), dy = (y2-y);
  11590. var slope = dy/dx;
  11591. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  11592. var dashIndex=0, draw=true;
  11593. while (distRemaining>=0.1){
  11594. var dashLength = dashArray[dashIndex++%dashCount];
  11595. if (dashLength > distRemaining) dashLength = distRemaining;
  11596. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  11597. if (dx<0) xStep = -xStep;
  11598. x += xStep;
  11599. y += slope*xStep;
  11600. this[draw ? 'lineTo' : 'moveTo'](x,y);
  11601. distRemaining -= dashLength;
  11602. draw = !draw;
  11603. }
  11604. };
  11605. // TODO: add diamond shape
  11606. }
  11607. /**
  11608. * @class Node
  11609. * A node. A node can be connected to other nodes via one or multiple edges.
  11610. * @param {object} properties An object containing properties for the node. All
  11611. * properties are optional, except for the id.
  11612. * {number} id Id of the node. Required
  11613. * {string} label Text label for the node
  11614. * {number} x Horizontal position of the node
  11615. * {number} y Vertical position of the node
  11616. * {string} shape Node shape, available:
  11617. * "database", "circle", "ellipse",
  11618. * "box", "image", "text", "dot",
  11619. * "star", "triangle", "triangleDown",
  11620. * "square"
  11621. * {string} image An image url
  11622. * {string} title An title text, can be HTML
  11623. * {anytype} group A group name or number
  11624. * @param {Graph.Images} imagelist A list with images. Only needed
  11625. * when the node has an image
  11626. * @param {Graph.Groups} grouplist A list with groups. Needed for
  11627. * retrieving group properties
  11628. * @param {Object} constants An object with default values for
  11629. * example for the color
  11630. */
  11631. function Node(properties, imagelist, grouplist, constants) {
  11632. this.selected = false;
  11633. this.edges = []; // all edges connected to this node
  11634. this.dynamicEdges = [];
  11635. this.reroutedEdges = {};
  11636. this.group = constants.nodes.group;
  11637. this.fontSize = constants.nodes.fontSize;
  11638. this.fontFace = constants.nodes.fontFace;
  11639. this.fontColor = constants.nodes.fontColor;
  11640. this.color = constants.nodes.color;
  11641. // set defaults for the properties
  11642. this.id = undefined;
  11643. this.shape = constants.nodes.shape;
  11644. this.image = constants.nodes.image;
  11645. this.x = 0;
  11646. this.y = 0;
  11647. this.xFixed = false;
  11648. this.yFixed = false;
  11649. this.radius = constants.nodes.radius;
  11650. this.baseRadiusValue = constants.nodes.radius;
  11651. this.radiusFixed = false;
  11652. this.radiusMin = constants.nodes.radiusMin;
  11653. this.radiusMax = constants.nodes.radiusMax;
  11654. this.imagelist = imagelist;
  11655. this.grouplist = grouplist;
  11656. this.setProperties(properties, constants);
  11657. // creating the variables for clustering
  11658. this.resetCluster();
  11659. this.dynamicEdgesLength = 0;
  11660. this.clusterSession = 0;
  11661. this.clusterSizeWidthFactor = constants.clustering.clusterSizeWidthFactor;
  11662. this.clusterSizeHeightFactor = constants.clustering.clusterSizeHeightFactor;
  11663. this.clusterSizeRadiusFactor = constants.clustering.clusterSizeRadiusFactor;
  11664. // mass, force, velocity
  11665. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  11666. this.fx = 0.0; // external force x
  11667. this.fy = 0.0; // external force y
  11668. this.vx = 0.0; // velocity x
  11669. this.vy = 0.0; // velocity y
  11670. this.minForce = constants.minForce;
  11671. this.damping = 0.9; // damping factor
  11672. this.graphScaleInv = 1;
  11673. }
  11674. /**
  11675. * (re)setting the clustering variables and objects
  11676. */
  11677. Node.prototype.resetCluster = function() {
  11678. // clustering variables
  11679. this.formationScale = undefined; // this is used to determine when to open the cluster
  11680. this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
  11681. this.containedNodes = {};
  11682. this.containedEdges = {};
  11683. this.clusterSessions = [];
  11684. };
  11685. /**
  11686. * Attach a edge to the node
  11687. * @param {Edge} edge
  11688. */
  11689. Node.prototype.attachEdge = function(edge) {
  11690. if (this.edges.indexOf(edge) == -1) {
  11691. this.edges.push(edge);
  11692. }
  11693. if (this.dynamicEdges.indexOf(edge) == -1) {
  11694. this.dynamicEdges.push(edge);
  11695. }
  11696. this.dynamicEdgesLength = this.dynamicEdges.length;
  11697. this._updateMass();
  11698. };
  11699. /**
  11700. * Detach a edge from the node
  11701. * @param {Edge} edge
  11702. */
  11703. Node.prototype.detachEdge = function(edge) {
  11704. var index = this.edges.indexOf(edge);
  11705. if (index != -1) {
  11706. this.edges.splice(index, 1);
  11707. this.dynamicEdges.splice(index, 1);
  11708. }
  11709. this.dynamicEdgesLength = this.dynamicEdges.length;
  11710. this._updateMass();
  11711. };
  11712. /**
  11713. * Update the nodes mass, which is determined by the number of edges connecting
  11714. * to it (more edges -> heavier node).
  11715. * @private
  11716. */
  11717. Node.prototype._updateMass = function() {
  11718. this.mass = 50 + 20 * this.edges.length; // kg
  11719. };
  11720. /**
  11721. * Set or overwrite properties for the node
  11722. * @param {Object} properties an object with properties
  11723. * @param {Object} constants and object with default, global properties
  11724. */
  11725. Node.prototype.setProperties = function(properties, constants) {
  11726. if (!properties) {
  11727. return;
  11728. }
  11729. // basic properties
  11730. if (properties.id !== undefined) {this.id = properties.id;}
  11731. if (properties.label !== undefined) {this.label = properties.label;}
  11732. if (properties.title !== undefined) {this.title = properties.title;}
  11733. if (properties.group !== undefined) {this.group = properties.group;}
  11734. if (properties.x !== undefined) {this.x = properties.x;}
  11735. if (properties.y !== undefined) {this.y = properties.y;}
  11736. if (properties.value !== undefined) {this.value = properties.value;}
  11737. if (this.id === undefined) {
  11738. throw "Node must have an id";
  11739. }
  11740. // copy group properties
  11741. if (this.group) {
  11742. var groupObj = this.grouplist.get(this.group);
  11743. for (var prop in groupObj) {
  11744. if (groupObj.hasOwnProperty(prop)) {
  11745. this[prop] = groupObj[prop];
  11746. }
  11747. }
  11748. }
  11749. // individual shape properties
  11750. if (properties.shape !== undefined) {this.shape = properties.shape;}
  11751. if (properties.image !== undefined) {this.image = properties.image;}
  11752. if (properties.radius !== undefined) {this.radius = properties.radius;}
  11753. if (properties.color !== undefined) {this.color = Node.parseColor(properties.color);}
  11754. if (properties.fontColor !== undefined) {this.fontColor = properties.fontColor;}
  11755. if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;}
  11756. if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;}
  11757. if (this.image !== undefined) {
  11758. if (this.imagelist) {
  11759. this.imageObj = this.imagelist.load(this.image);
  11760. }
  11761. else {
  11762. throw "No imagelist provided";
  11763. }
  11764. }
  11765. this.xFixed = this.xFixed || (properties.x !== undefined);
  11766. this.yFixed = this.yFixed || (properties.y !== undefined);
  11767. this.radiusFixed = this.radiusFixed || (properties.radius !== undefined);
  11768. if (this.shape == 'image') {
  11769. this.radiusMin = constants.nodes.widthMin;
  11770. this.radiusMax = constants.nodes.widthMax;
  11771. }
  11772. // choose draw method depending on the shape
  11773. switch (this.shape) {
  11774. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  11775. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  11776. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  11777. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  11778. // TODO: add diamond shape
  11779. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  11780. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  11781. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  11782. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  11783. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  11784. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  11785. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  11786. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  11787. }
  11788. // reset the size of the node, this can be changed
  11789. this._reset();
  11790. };
  11791. /**
  11792. * Parse a color property into an object with border, background, and
  11793. * hightlight colors
  11794. * @param {Object | String} color
  11795. * @return {Object} colorObject
  11796. */
  11797. Node.parseColor = function(color) {
  11798. var c;
  11799. if (util.isString(color)) {
  11800. c = {
  11801. border: color,
  11802. background: color,
  11803. highlight: {
  11804. border: color,
  11805. background: color
  11806. }
  11807. };
  11808. // TODO: automatically generate a nice highlight color
  11809. }
  11810. else {
  11811. c = {};
  11812. c.background = color.background || 'white';
  11813. c.border = color.border || c.background;
  11814. if (util.isString(color.highlight)) {
  11815. c.highlight = {
  11816. border: color.highlight,
  11817. background: color.highlight
  11818. }
  11819. }
  11820. else {
  11821. c.highlight = {};
  11822. c.highlight.background = color.highlight && color.highlight.background || c.background;
  11823. c.highlight.border = color.highlight && color.highlight.border || c.border;
  11824. }
  11825. }
  11826. return c;
  11827. };
  11828. /**
  11829. * select this node
  11830. */
  11831. Node.prototype.select = function() {
  11832. this.selected = true;
  11833. this._reset();
  11834. };
  11835. /**
  11836. * unselect this node
  11837. */
  11838. Node.prototype.unselect = function() {
  11839. this.selected = false;
  11840. this._reset();
  11841. };
  11842. /**
  11843. * Reset the calculated size of the node, forces it to recalculate its size
  11844. */
  11845. Node.prototype.clearSizeCache = function() {
  11846. this._reset();
  11847. };
  11848. /**
  11849. * Reset the calculated size of the node, forces it to recalculate its size
  11850. * @private
  11851. */
  11852. Node.prototype._reset = function() {
  11853. this.width = undefined;
  11854. this.height = undefined;
  11855. };
  11856. /**
  11857. * get the title of this node.
  11858. * @return {string} title The title of the node, or undefined when no title
  11859. * has been set.
  11860. */
  11861. Node.prototype.getTitle = function() {
  11862. return this.title;
  11863. };
  11864. /**
  11865. * Calculate the distance to the border of the Node
  11866. * @param {CanvasRenderingContext2D} ctx
  11867. * @param {Number} angle Angle in radians
  11868. * @returns {number} distance Distance to the border in pixels
  11869. */
  11870. Node.prototype.distanceToBorder = function (ctx, angle) {
  11871. var borderWidth = 1;
  11872. if (!this.width) {
  11873. this.resize(ctx);
  11874. }
  11875. //noinspection FallthroughInSwitchStatementJS
  11876. switch (this.shape) {
  11877. case 'circle':
  11878. case 'dot':
  11879. return this.radius + borderWidth;
  11880. case 'ellipse':
  11881. var a = this.width / 2;
  11882. var b = this.height / 2;
  11883. var w = (Math.sin(angle) * a);
  11884. var h = (Math.cos(angle) * b);
  11885. return a * b / Math.sqrt(w * w + h * h);
  11886. // TODO: implement distanceToBorder for database
  11887. // TODO: implement distanceToBorder for triangle
  11888. // TODO: implement distanceToBorder for triangleDown
  11889. case 'box':
  11890. case 'image':
  11891. case 'text':
  11892. default:
  11893. if (this.width) {
  11894. return Math.min(
  11895. Math.abs(this.width / 2 / Math.cos(angle)),
  11896. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  11897. // TODO: reckon with border radius too in case of box
  11898. }
  11899. else {
  11900. return 0;
  11901. }
  11902. }
  11903. // TODO: implement calculation of distance to border for all shapes
  11904. };
  11905. /**
  11906. * Set forces acting on the node
  11907. * @param {number} fx Force in horizontal direction
  11908. * @param {number} fy Force in vertical direction
  11909. */
  11910. Node.prototype._setForce = function(fx, fy) {
  11911. this.fx = fx;
  11912. this.fy = fy;
  11913. };
  11914. /**
  11915. * Add forces acting on the node
  11916. * @param {number} fx Force in horizontal direction
  11917. * @param {number} fy Force in vertical direction
  11918. * @private
  11919. */
  11920. Node.prototype._addForce = function(fx, fy) {
  11921. this.fx += fx;
  11922. this.fy += fy;
  11923. };
  11924. /**
  11925. * Perform one discrete step for the node
  11926. * @param {number} interval Time interval in seconds
  11927. */
  11928. Node.prototype.discreteStep = function(interval) {
  11929. if (!this.xFixed) {
  11930. var dx = -this.damping * this.vx; // damping force
  11931. var ax = (this.fx + dx) / this.mass; // acceleration
  11932. this.vx += ax / interval; // velocity
  11933. this.x += this.vx / interval; // position
  11934. }
  11935. if (!this.yFixed) {
  11936. var dy = -this.damping * this.vy; // damping force
  11937. var ay = (this.fy + dy) / this.mass; // acceleration
  11938. this.vy += ay / interval; // velocity
  11939. this.y += this.vy / interval; // position
  11940. }
  11941. };
  11942. /**
  11943. * Check if this node has a fixed x and y position
  11944. * @return {boolean} true if fixed, false if not
  11945. */
  11946. Node.prototype.isFixed = function() {
  11947. return (this.xFixed && this.yFixed);
  11948. };
  11949. /**
  11950. * Check if this node is moving
  11951. * @param {number} vmin the minimum velocity considered as "moving"
  11952. * @return {boolean} true if moving, false if it has no velocity
  11953. */
  11954. // TODO: replace this method with calculating the kinetic energy
  11955. Node.prototype.isMoving = function(vmin) {
  11956. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  11957. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  11958. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  11959. };
  11960. /**
  11961. * check if this node is selecte
  11962. * @return {boolean} selected True if node is selected, else false
  11963. */
  11964. Node.prototype.isSelected = function() {
  11965. return this.selected;
  11966. };
  11967. /**
  11968. * Retrieve the value of the node. Can be undefined
  11969. * @return {Number} value
  11970. */
  11971. Node.prototype.getValue = function() {
  11972. return this.value;
  11973. };
  11974. /**
  11975. * Calculate the distance from the nodes location to the given location (x,y)
  11976. * @param {Number} x
  11977. * @param {Number} y
  11978. * @return {Number} value
  11979. */
  11980. Node.prototype.getDistance = function(x, y) {
  11981. var dx = this.x - x,
  11982. dy = this.y - y;
  11983. return Math.sqrt(dx * dx + dy * dy);
  11984. };
  11985. /**
  11986. * Adjust the value range of the node. The node will adjust it's radius
  11987. * based on its value.
  11988. * @param {Number} min
  11989. * @param {Number} max
  11990. */
  11991. Node.prototype.setValueRange = function(min, max) {
  11992. if (!this.radiusFixed && this.value !== undefined) {
  11993. if (max == min) {
  11994. this.radius = (this.radiusMin + this.radiusMax) / 2;
  11995. }
  11996. else {
  11997. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  11998. this.radius = (this.value - min) * scale + this.radiusMin;
  11999. }
  12000. }
  12001. };
  12002. /**
  12003. * Draw this node in the given canvas
  12004. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12005. * @param {CanvasRenderingContext2D} ctx
  12006. */
  12007. Node.prototype.draw = function(ctx) {
  12008. throw "Draw method not initialized for node";
  12009. };
  12010. /**
  12011. * Recalculate the size of this node in the given canvas
  12012. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12013. * @param {CanvasRenderingContext2D} ctx
  12014. */
  12015. Node.prototype.resize = function(ctx) {
  12016. throw "Resize method not initialized for node";
  12017. };
  12018. /**
  12019. * Check if this object is overlapping with the provided object
  12020. * @param {Object} obj an object with parameters left, top, right, bottom
  12021. * @return {boolean} True if location is located on node
  12022. */
  12023. Node.prototype.isOverlappingWith = function(obj) {
  12024. return (this.left < obj.right &&
  12025. this.left + this.width > obj.left &&
  12026. this.top < obj.bottom &&
  12027. this.top + this.height > obj.top);
  12028. };
  12029. Node.prototype._resizeImage = function (ctx) {
  12030. // TODO: pre calculate the image size
  12031. if (!this.width) { // undefined or 0
  12032. var width, height;
  12033. if (this.value) {
  12034. var scale = this.imageObj.height / this.imageObj.width;
  12035. width = this.radius || this.imageObj.width;
  12036. height = this.radius * scale || this.imageObj.height;
  12037. }
  12038. else {
  12039. width = this.imageObj.width;
  12040. height = this.imageObj.height;
  12041. }
  12042. this.width = width;
  12043. this.height = height;
  12044. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  12045. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  12046. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  12047. }
  12048. };
  12049. Node.prototype._drawImage = function (ctx) {
  12050. this._resizeImage(ctx);
  12051. this.left = this.x - this.width / 2;
  12052. this.top = this.y - this.height / 2;
  12053. var yLabel;
  12054. if (this.imageObj) {
  12055. // draw the shade
  12056. if (this.clusterSize > 1) {
  12057. var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
  12058. lineWidth *= this.graphScaleInv;
  12059. lineWidth = Math.min(0.2 * this.width,lineWidth);
  12060. ctx.globalAlpha = 0.5;
  12061. ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
  12062. }
  12063. ctx.globalAlpha = 1.0;
  12064. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  12065. yLabel = this.y + this.height / 2;
  12066. }
  12067. else {
  12068. // image still loading... just draw the label for now
  12069. yLabel = this.y;
  12070. }
  12071. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  12072. };
  12073. Node.prototype._resizeBox = function (ctx) {
  12074. if (!this.width) {
  12075. var margin = 5;
  12076. var textSize = this.getTextSize(ctx);
  12077. this.width = textSize.width + 2 * margin;
  12078. this.height = textSize.height + 2 * margin;
  12079. this.width += this.clusterSize * 0.5 * this.clusterSizeWidthFactor;
  12080. this.height += this.clusterSize * 0.5 * this.clusterSizeHeightFactor;
  12081. //this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  12082. }
  12083. };
  12084. Node.prototype._drawBox = function (ctx) {
  12085. this._resizeBox(ctx);
  12086. this.left = this.x - this.width / 2;
  12087. this.top = this.y - this.height / 2;
  12088. var clusterLineWidth = 2.5;
  12089. var selectionLineWidth = 2;
  12090. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  12091. // draw the outer border
  12092. if (this.clusterSize > 1) {
  12093. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12094. ctx.lineWidth *= this.graphScaleInv;
  12095. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12096. ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.radius);
  12097. ctx.stroke();
  12098. }
  12099. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12100. ctx.lineWidth *= this.graphScaleInv;
  12101. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12102. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  12103. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  12104. ctx.fill();
  12105. ctx.stroke();
  12106. this._label(ctx, this.label, this.x, this.y);
  12107. };
  12108. Node.prototype._resizeDatabase = function (ctx) {
  12109. if (!this.width) {
  12110. var margin = 5;
  12111. var textSize = this.getTextSize(ctx);
  12112. var size = textSize.width + 2 * margin;
  12113. this.width = size;
  12114. this.height = size;
  12115. // scaling used for clustering
  12116. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  12117. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  12118. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  12119. }
  12120. };
  12121. Node.prototype._drawDatabase = function (ctx) {
  12122. this._resizeDatabase(ctx);
  12123. this.left = this.x - this.width / 2;
  12124. this.top = this.y - this.height / 2;
  12125. var clusterLineWidth = 2.5;
  12126. var selectionLineWidth = 2;
  12127. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  12128. // draw the outer border
  12129. if (this.clusterSize > 1) {
  12130. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12131. ctx.lineWidth *= this.graphScaleInv;
  12132. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12133. 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);
  12134. ctx.stroke();
  12135. }
  12136. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12137. ctx.lineWidth *= this.graphScaleInv;
  12138. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12139. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  12140. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  12141. ctx.fill();
  12142. ctx.stroke();
  12143. this._label(ctx, this.label, this.x, this.y);
  12144. };
  12145. Node.prototype._resizeCircle = function (ctx) {
  12146. if (!this.width) {
  12147. var margin = 5;
  12148. var textSize = this.getTextSize(ctx);
  12149. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  12150. this.radius = diameter / 2;
  12151. this.width = diameter;
  12152. this.height = diameter;
  12153. // scaling used for clustering
  12154. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  12155. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  12156. this.radius += this.clusterSize * 0.5*this.clusterSizeRadiusFactor;
  12157. }
  12158. };
  12159. Node.prototype._drawCircle = function (ctx) {
  12160. this._resizeCircle(ctx);
  12161. this.left = this.x - this.width / 2;
  12162. this.top = this.y - this.height / 2;
  12163. var clusterLineWidth = 2.5;
  12164. var selectionLineWidth = 2;
  12165. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  12166. // draw the outer border
  12167. if (this.clusterSize > 1) {
  12168. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12169. ctx.lineWidth *= this.graphScaleInv;
  12170. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12171. ctx.circle(this.x, this.y, this.radius+2*ctx.lineWidth);
  12172. ctx.stroke();
  12173. }
  12174. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12175. ctx.lineWidth *= this.graphScaleInv;
  12176. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12177. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  12178. ctx.circle(this.x, this.y, this.radius);
  12179. ctx.fill();
  12180. ctx.stroke();
  12181. this._label(ctx, this.label, this.x, this.y);
  12182. };
  12183. Node.prototype._resizeEllipse = function (ctx) {
  12184. if (!this.width) {
  12185. var textSize = this.getTextSize(ctx);
  12186. this.width = textSize.width * 1.5;
  12187. this.height = textSize.height * 2;
  12188. if (this.width < this.height) {
  12189. this.width = this.height;
  12190. }
  12191. // scaling used for clustering
  12192. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  12193. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  12194. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  12195. }
  12196. };
  12197. Node.prototype._drawEllipse = function (ctx) {
  12198. this._resizeEllipse(ctx);
  12199. this.left = this.x - this.width / 2;
  12200. this.top = this.y - this.height / 2;
  12201. var clusterLineWidth = 2.5;
  12202. var selectionLineWidth = 2;
  12203. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  12204. // draw the outer border
  12205. if (this.clusterSize > 1) {
  12206. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12207. ctx.lineWidth *= this.graphScaleInv;
  12208. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12209. ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
  12210. ctx.stroke();
  12211. }
  12212. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12213. ctx.lineWidth *= this.graphScaleInv;
  12214. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12215. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  12216. ctx.ellipse(this.left, this.top, this.width, this.height);
  12217. ctx.fill();
  12218. ctx.stroke();
  12219. this._label(ctx, this.label, this.x, this.y);
  12220. };
  12221. Node.prototype._drawDot = function (ctx) {
  12222. this._drawShape(ctx, 'circle');
  12223. };
  12224. Node.prototype._drawTriangle = function (ctx) {
  12225. this._drawShape(ctx, 'triangle');
  12226. };
  12227. Node.prototype._drawTriangleDown = function (ctx) {
  12228. this._drawShape(ctx, 'triangleDown');
  12229. };
  12230. Node.prototype._drawSquare = function (ctx) {
  12231. this._drawShape(ctx, 'square');
  12232. };
  12233. Node.prototype._drawStar = function (ctx) {
  12234. this._drawShape(ctx, 'star');
  12235. };
  12236. Node.prototype._resizeShape = function (ctx) {
  12237. if (!this.width) {
  12238. this.radius = this.baseRadiusValue;
  12239. var size = 2 * this.radius;
  12240. this.width = size;
  12241. this.height = size;
  12242. // scaling used for clustering
  12243. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  12244. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  12245. this.radius += this.clusterSize * 0.5 * this.clusterSizeRadiusFactor;
  12246. }
  12247. };
  12248. Node.prototype._drawShape = function (ctx, shape) {
  12249. this._resizeShape(ctx);
  12250. this.left = this.x - this.width / 2;
  12251. this.top = this.y - this.height / 2;
  12252. var clusterLineWidth = 2.5;
  12253. var selectionLineWidth = 2;
  12254. var radiusMultiplier = 2;
  12255. // choose draw method depending on the shape
  12256. switch (shape) {
  12257. case 'dot': radiusMultiplier = 2; break;
  12258. case 'square': radiusMultiplier = 2; break;
  12259. case 'triangle': radiusMultiplier = 3; break;
  12260. case 'triangleDown': radiusMultiplier = 3; break;
  12261. case 'star': radiusMultiplier = 4; break;
  12262. }
  12263. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  12264. // draw the outer border
  12265. if (this.clusterSize > 1) {
  12266. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12267. ctx.lineWidth *= this.graphScaleInv;
  12268. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12269. ctx[shape](this.x, this.y, this.radius + radiusMultiplier * ctx.lineWidth);
  12270. ctx.stroke();
  12271. }
  12272. ctx.lineWidth = (this.selected ? selectionLineWidth : 1.0) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
  12273. ctx.lineWidth *= this.graphScaleInv;
  12274. ctx.lineWidth = Math.min(0.1 * this.width,ctx.lineWidth);
  12275. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  12276. ctx[shape](this.x, this.y, this.radius);
  12277. ctx.fill();
  12278. ctx.stroke();
  12279. if (this.label) {
  12280. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  12281. }
  12282. };
  12283. Node.prototype._resizeText = function (ctx) {
  12284. if (!this.width) {
  12285. var margin = 5;
  12286. var textSize = this.getTextSize(ctx);
  12287. this.width = textSize.width + 2 * margin;
  12288. this.height = textSize.height + 2 * margin;
  12289. // scaling used for clustering
  12290. this.width += this.clusterSize * this.clusterSizeWidthFactor;
  12291. this.height += this.clusterSize * this.clusterSizeHeightFactor;
  12292. this.radius += this.clusterSize * this.clusterSizeRadiusFactor;
  12293. }
  12294. };
  12295. Node.prototype._drawText = function (ctx) {
  12296. this._resizeText(ctx);
  12297. this.left = this.x - this.width / 2;
  12298. this.top = this.y - this.height / 2;
  12299. this._label(ctx, this.label, this.x, this.y);
  12300. };
  12301. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  12302. if (text) {
  12303. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  12304. ctx.fillStyle = this.fontColor || "black";
  12305. ctx.textAlign = align || "center";
  12306. ctx.textBaseline = baseline || "middle";
  12307. var lines = text.split('\n'),
  12308. lineCount = lines.length,
  12309. fontSize = (this.fontSize + 4),
  12310. yLine = y + (1 - lineCount) / 2 * fontSize;
  12311. for (var i = 0; i < lineCount; i++) {
  12312. ctx.fillText(lines[i], x, yLine);
  12313. yLine += fontSize;
  12314. }
  12315. }
  12316. };
  12317. Node.prototype.getTextSize = function(ctx) {
  12318. if (this.label !== undefined) {
  12319. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  12320. var lines = this.label.split('\n'),
  12321. height = (this.fontSize + 4) * lines.length,
  12322. width = 0;
  12323. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  12324. width = Math.max(width, ctx.measureText(lines[i]).width);
  12325. }
  12326. return {"width": width, "height": height};
  12327. }
  12328. else {
  12329. return {"width": 0, "height": 0};
  12330. }
  12331. };
  12332. /**
  12333. * This allows the zoom level of the graph to influence the rendering
  12334. *
  12335. * @param scale
  12336. */
  12337. Node.prototype.setScale = function(scale) {
  12338. this.graphScaleInv = 1.0/scale;
  12339. };
  12340. /**
  12341. * This function updates the damping parameter for clusters, based ont he
  12342. *
  12343. * @param {Integer} numberOfNodes
  12344. */
  12345. Node.prototype.updateDamping = function(numberOfNodes) {
  12346. this.damping = 0.8 + 0.1*this.clusterSize * (1 + 2/Math.pow(numberOfNodes,2));
  12347. };
  12348. /**
  12349. * set the velocity at 0. Is called when this node is contained in another during clustering
  12350. */
  12351. Node.prototype.clearVelocity = function() {
  12352. this.vx = 0;
  12353. this.vy = 0;
  12354. };
  12355. /**
  12356. * Basic preservation of (kinectic) energy
  12357. *
  12358. * @param massBeforeClustering
  12359. */
  12360. Node.prototype.updateVelocity = function(massBeforeClustering) {
  12361. var energyBefore = this.vx * this.vx * massBeforeClustering;
  12362. this.vx = Math.sqrt(energyBefore/this.mass);
  12363. energyBefore = this.vy * this.vy * massBeforeClustering;
  12364. this.vy = Math.sqrt(energyBefore/this.mass);
  12365. };
  12366. /**
  12367. * @class Edge
  12368. *
  12369. * A edge connects two nodes
  12370. * @param {Object} properties Object with properties. Must contain
  12371. * At least properties from and to.
  12372. * Available properties: from (number),
  12373. * to (number), label (string, color (string),
  12374. * width (number), style (string),
  12375. * length (number), title (string)
  12376. * @param {Graph} graph A graph object, used to find and edge to
  12377. * nodes.
  12378. * @param {Object} constants An object with default values for
  12379. * example for the color
  12380. */
  12381. function Edge (properties, graph, constants) {
  12382. if (!graph) {
  12383. throw "No graph provided";
  12384. }
  12385. this.graph = graph;
  12386. // initialize constants
  12387. this.widthMin = constants.edges.widthMin;
  12388. this.widthMax = constants.edges.widthMax;
  12389. // initialize variables
  12390. this.id = undefined;
  12391. this.fromId = undefined;
  12392. this.toId = undefined;
  12393. this.style = constants.edges.style;
  12394. this.title = undefined;
  12395. this.width = constants.edges.width;
  12396. this.value = undefined;
  12397. this.length = constants.edges.length;
  12398. this.from = null; // a node
  12399. this.to = null; // a node
  12400. // we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
  12401. // by storing the original information we can revert to the original connection when the cluser is opened.
  12402. this.originalFromID = [];
  12403. this.originalToID = [];
  12404. this.connected = false;
  12405. // Added to support dashed lines
  12406. // David Jordan
  12407. // 2012-08-08
  12408. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  12409. this.stiffness = undefined; // depends on the length of the edge
  12410. this.color = constants.edges.color;
  12411. this.widthFixed = false;
  12412. this.lengthFixed = false;
  12413. this.setProperties(properties, constants);
  12414. };
  12415. /**
  12416. * Set or overwrite properties for the edge
  12417. * @param {Object} properties an object with properties
  12418. * @param {Object} constants and object with default, global properties
  12419. */
  12420. Edge.prototype.setProperties = function(properties, constants) {
  12421. if (!properties) {
  12422. return;
  12423. }
  12424. if (properties.from != undefined) {this.fromId = properties.from;}
  12425. if (properties.to != undefined) {this.toId = properties.to;}
  12426. if (properties.id != undefined) {this.id = properties.id;}
  12427. if (properties.style != undefined) {this.style = properties.style;}
  12428. if (properties.label != undefined) {this.label = properties.label;}
  12429. if (this.label) {
  12430. this.fontSize = constants.edges.fontSize;
  12431. this.fontFace = constants.edges.fontFace;
  12432. this.fontColor = constants.edges.fontColor;
  12433. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  12434. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  12435. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  12436. }
  12437. if (properties.title != undefined) {this.title = properties.title;}
  12438. if (properties.width != undefined) {this.width = properties.width;}
  12439. if (properties.value != undefined) {this.value = properties.value;}
  12440. if (properties.length != undefined) {this.length = properties.length;}
  12441. // Added to support dashed lines
  12442. // David Jordan
  12443. // 2012-08-08
  12444. if (properties.dash) {
  12445. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  12446. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  12447. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  12448. }
  12449. if (properties.color != undefined) {this.color = properties.color;}
  12450. // A node is connected when it has a from and to node.
  12451. this.connect();
  12452. this.widthFixed = this.widthFixed || (properties.width != undefined);
  12453. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  12454. this.stiffness = 1 / this.length;
  12455. // set draw method based on style
  12456. switch (this.style) {
  12457. case 'line': this.draw = this._drawLine; break;
  12458. case 'arrow': this.draw = this._drawArrow; break;
  12459. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  12460. case 'dash-line': this.draw = this._drawDashLine; break;
  12461. default: this.draw = this._drawLine; break;
  12462. }
  12463. };
  12464. /**
  12465. * Connect an edge to its nodes
  12466. */
  12467. Edge.prototype.connect = function () {
  12468. this.disconnect();
  12469. this.from = this.graph.nodes[this.fromId] || null;
  12470. this.to = this.graph.nodes[this.toId] || null;
  12471. this.connected = (this.from && this.to);
  12472. if (this.connected) {
  12473. this.from.attachEdge(this);
  12474. this.to.attachEdge(this);
  12475. }
  12476. else {
  12477. if (this.from) {
  12478. this.from.detachEdge(this);
  12479. }
  12480. if (this.to) {
  12481. this.to.detachEdge(this);
  12482. }
  12483. }
  12484. };
  12485. /**
  12486. * Disconnect an edge from its nodes
  12487. */
  12488. Edge.prototype.disconnect = function () {
  12489. if (this.from) {
  12490. this.from.detachEdge(this);
  12491. this.from = null;
  12492. }
  12493. if (this.to) {
  12494. this.to.detachEdge(this);
  12495. this.to = null;
  12496. }
  12497. this.connected = false;
  12498. };
  12499. /**
  12500. * get the title of this edge.
  12501. * @return {string} title The title of the edge, or undefined when no title
  12502. * has been set.
  12503. */
  12504. Edge.prototype.getTitle = function() {
  12505. return this.title;
  12506. };
  12507. /**
  12508. * Retrieve the value of the edge. Can be undefined
  12509. * @return {Number} value
  12510. */
  12511. Edge.prototype.getValue = function() {
  12512. return this.value;
  12513. };
  12514. /**
  12515. * Adjust the value range of the edge. The edge will adjust it's width
  12516. * based on its value.
  12517. * @param {Number} min
  12518. * @param {Number} max
  12519. */
  12520. Edge.prototype.setValueRange = function(min, max) {
  12521. if (!this.widthFixed && this.value !== undefined) {
  12522. var scale = (this.widthMax - this.widthMin) / (max - min);
  12523. this.width = (this.value - min) * scale + this.widthMin;
  12524. }
  12525. };
  12526. /**
  12527. * Redraw a edge
  12528. * Draw this edge in the given canvas
  12529. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12530. * @param {CanvasRenderingContext2D} ctx
  12531. */
  12532. Edge.prototype.draw = function(ctx) {
  12533. throw "Method draw not initialized in edge";
  12534. };
  12535. /**
  12536. * Check if this object is overlapping with the provided object
  12537. * @param {Object} obj an object with parameters left, top
  12538. * @return {boolean} True if location is located on the edge
  12539. */
  12540. Edge.prototype.isOverlappingWith = function(obj) {
  12541. var distMax = 10;
  12542. var xFrom = this.from.x;
  12543. var yFrom = this.from.y;
  12544. var xTo = this.to.x;
  12545. var yTo = this.to.y;
  12546. var xObj = obj.left;
  12547. var yObj = obj.top;
  12548. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  12549. return (dist < distMax);
  12550. };
  12551. /**
  12552. * Redraw a edge as a line
  12553. * Draw this edge in the given canvas
  12554. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12555. * @param {CanvasRenderingContext2D} ctx
  12556. * @private
  12557. */
  12558. Edge.prototype._drawLine = function(ctx) {
  12559. // set style
  12560. ctx.strokeStyle = this.color;
  12561. ctx.lineWidth = this._getLineWidth();
  12562. var point;
  12563. if (this.from != this.to) {
  12564. // draw line
  12565. this._line(ctx);
  12566. // draw label
  12567. if (this.label) {
  12568. point = this._pointOnLine(0.5);
  12569. this._label(ctx, this.label, point.x, point.y);
  12570. }
  12571. }
  12572. else {
  12573. var x, y;
  12574. var radius = this.length / 4;
  12575. var node = this.from;
  12576. if (!node.width) {
  12577. node.resize(ctx);
  12578. }
  12579. if (node.width > node.height) {
  12580. x = node.x + node.width / 2;
  12581. y = node.y - radius;
  12582. }
  12583. else {
  12584. x = node.x + radius;
  12585. y = node.y - node.height / 2;
  12586. }
  12587. this._circle(ctx, x, y, radius);
  12588. point = this._pointOnCircle(x, y, radius, 0.5);
  12589. this._label(ctx, this.label, point.x, point.y);
  12590. }
  12591. };
  12592. /**
  12593. * Get the line width of the edge. Depends on width and whether one of the
  12594. * connected nodes is selected.
  12595. * @return {Number} width
  12596. * @private
  12597. */
  12598. Edge.prototype._getLineWidth = function() {
  12599. if (this.from.selected || this.to.selected) {
  12600. return Math.min(this.width * 2, this.widthMax)*this.graphScaleInv;
  12601. }
  12602. else {
  12603. return this.width*this.graphScaleInv;
  12604. }
  12605. };
  12606. /**
  12607. * Draw a line between two nodes
  12608. * @param {CanvasRenderingContext2D} ctx
  12609. * @private
  12610. */
  12611. Edge.prototype._line = function (ctx) {
  12612. // draw a straight line
  12613. ctx.beginPath();
  12614. ctx.moveTo(this.from.x, this.from.y);
  12615. ctx.lineTo(this.to.x, this.to.y);
  12616. ctx.stroke();
  12617. };
  12618. /**
  12619. * Draw a line from a node to itself, a circle
  12620. * @param {CanvasRenderingContext2D} ctx
  12621. * @param {Number} x
  12622. * @param {Number} y
  12623. * @param {Number} radius
  12624. * @private
  12625. */
  12626. Edge.prototype._circle = function (ctx, x, y, radius) {
  12627. // draw a circle
  12628. ctx.beginPath();
  12629. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  12630. ctx.stroke();
  12631. };
  12632. /**
  12633. * Draw label with white background and with the middle at (x, y)
  12634. * @param {CanvasRenderingContext2D} ctx
  12635. * @param {String} text
  12636. * @param {Number} x
  12637. * @param {Number} y
  12638. * @private
  12639. */
  12640. Edge.prototype._label = function (ctx, text, x, y) {
  12641. if (text) {
  12642. // TODO: cache the calculated size
  12643. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  12644. this.fontSize + "px " + this.fontFace;
  12645. ctx.fillStyle = 'white';
  12646. var width = ctx.measureText(text).width;
  12647. var height = this.fontSize;
  12648. var left = x - width / 2;
  12649. var top = y - height / 2;
  12650. ctx.fillRect(left, top, width, height);
  12651. // draw text
  12652. ctx.fillStyle = this.fontColor || "black";
  12653. ctx.textAlign = "left";
  12654. ctx.textBaseline = "top";
  12655. ctx.fillText(text, left, top);
  12656. }
  12657. };
  12658. /**
  12659. * Redraw a edge as a dashed line
  12660. * Draw this edge in the given canvas
  12661. * @author David Jordan
  12662. * @date 2012-08-08
  12663. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12664. * @param {CanvasRenderingContext2D} ctx
  12665. * @private
  12666. */
  12667. Edge.prototype._drawDashLine = function(ctx) {
  12668. // set style
  12669. ctx.strokeStyle = this.color;
  12670. ctx.lineWidth = this._getLineWidth();
  12671. // draw dashed line
  12672. ctx.beginPath();
  12673. ctx.lineCap = 'round';
  12674. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  12675. {
  12676. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  12677. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  12678. }
  12679. 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
  12680. {
  12681. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  12682. [this.dash.length,this.dash.gap]);
  12683. }
  12684. else //If all else fails draw a line
  12685. {
  12686. ctx.moveTo(this.from.x, this.from.y);
  12687. ctx.lineTo(this.to.x, this.to.y);
  12688. }
  12689. ctx.stroke();
  12690. // draw label
  12691. if (this.label) {
  12692. var point = this._pointOnLine(0.5);
  12693. this._label(ctx, this.label, point.x, point.y);
  12694. }
  12695. };
  12696. /**
  12697. * Get a point on a line
  12698. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  12699. * @return {Object} point
  12700. * @private
  12701. */
  12702. Edge.prototype._pointOnLine = function (percentage) {
  12703. return {
  12704. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  12705. y: (1 - percentage) * this.from.y + percentage * this.to.y
  12706. }
  12707. };
  12708. /**
  12709. * Get a point on a circle
  12710. * @param {Number} x
  12711. * @param {Number} y
  12712. * @param {Number} radius
  12713. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  12714. * @return {Object} point
  12715. * @private
  12716. */
  12717. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  12718. var angle = (percentage - 3/8) * 2 * Math.PI;
  12719. return {
  12720. x: x + radius * Math.cos(angle),
  12721. y: y - radius * Math.sin(angle)
  12722. }
  12723. };
  12724. /**
  12725. * Redraw a edge as a line with an arrow halfway the line
  12726. * Draw this edge in the given canvas
  12727. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12728. * @param {CanvasRenderingContext2D} ctx
  12729. * @private
  12730. */
  12731. Edge.prototype._drawArrowCenter = function(ctx) {
  12732. var point;
  12733. // set style
  12734. ctx.strokeStyle = this.color;
  12735. ctx.fillStyle = this.color;
  12736. ctx.lineWidth = this._getLineWidth();
  12737. if (this.from != this.to) {
  12738. // draw line
  12739. this._line(ctx);
  12740. // draw an arrow halfway the line
  12741. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12742. var length = 10 + 5 * this.width; // TODO: make customizable?
  12743. point = this._pointOnLine(0.5);
  12744. ctx.arrow(point.x, point.y, angle, length);
  12745. ctx.fill();
  12746. ctx.stroke();
  12747. // draw label
  12748. if (this.label) {
  12749. point = this._pointOnLine(0.5);
  12750. this._label(ctx, this.label, point.x, point.y);
  12751. }
  12752. }
  12753. else {
  12754. // draw circle
  12755. var x, y;
  12756. var radius = this.length / 4;
  12757. var node = this.from;
  12758. if (!node.width) {
  12759. node.resize(ctx);
  12760. }
  12761. if (node.width > node.height) {
  12762. x = node.x + node.width / 2;
  12763. y = node.y - radius;
  12764. }
  12765. else {
  12766. x = node.x + radius;
  12767. y = node.y - node.height / 2;
  12768. }
  12769. this._circle(ctx, x, y, radius);
  12770. // draw all arrows
  12771. var angle = 0.2 * Math.PI;
  12772. var length = 10 + 5 * this.width; // TODO: make customizable?
  12773. point = this._pointOnCircle(x, y, radius, 0.5);
  12774. ctx.arrow(point.x, point.y, angle, length);
  12775. ctx.fill();
  12776. ctx.stroke();
  12777. // draw label
  12778. if (this.label) {
  12779. point = this._pointOnCircle(x, y, radius, 0.5);
  12780. this._label(ctx, this.label, point.x, point.y);
  12781. }
  12782. }
  12783. };
  12784. /**
  12785. * Redraw a edge as a line with an arrow
  12786. * Draw this edge in the given canvas
  12787. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  12788. * @param {CanvasRenderingContext2D} ctx
  12789. * @private
  12790. */
  12791. Edge.prototype._drawArrow = function(ctx) {
  12792. // set style
  12793. ctx.strokeStyle = this.color;
  12794. ctx.fillStyle = this.color;
  12795. ctx.lineWidth = this._getLineWidth();
  12796. // draw line
  12797. var angle, length;
  12798. if (this.from != this.to) {
  12799. // calculate length and angle of the line
  12800. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  12801. var dx = (this.to.x - this.from.x);
  12802. var dy = (this.to.y - this.from.y);
  12803. var lEdge = Math.sqrt(dx * dx + dy * dy);
  12804. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  12805. var pFrom = (lEdge - lFrom) / lEdge;
  12806. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  12807. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  12808. var lTo = this.to.distanceToBorder(ctx, angle);
  12809. var pTo = (lEdge - lTo) / lEdge;
  12810. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  12811. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  12812. ctx.beginPath();
  12813. ctx.moveTo(xFrom, yFrom);
  12814. ctx.lineTo(xTo, yTo);
  12815. ctx.stroke();
  12816. // draw arrow at the end of the line
  12817. length = 10 + 5 * this.width; // TODO: make customizable?
  12818. ctx.arrow(xTo, yTo, angle, length);
  12819. ctx.fill();
  12820. ctx.stroke();
  12821. // draw label
  12822. if (this.label) {
  12823. var point = this._pointOnLine(0.5);
  12824. this._label(ctx, this.label, point.x, point.y);
  12825. }
  12826. }
  12827. else {
  12828. // draw circle
  12829. var node = this.from;
  12830. var x, y, arrow;
  12831. var radius = this.length / 4;
  12832. if (!node.width) {
  12833. node.resize(ctx);
  12834. }
  12835. if (node.width > node.height) {
  12836. x = node.x + node.width / 2;
  12837. y = node.y - radius;
  12838. arrow = {
  12839. x: x,
  12840. y: node.y,
  12841. angle: 0.9 * Math.PI
  12842. };
  12843. }
  12844. else {
  12845. x = node.x + radius;
  12846. y = node.y - node.height / 2;
  12847. arrow = {
  12848. x: node.x,
  12849. y: y,
  12850. angle: 0.6 * Math.PI
  12851. };
  12852. }
  12853. ctx.beginPath();
  12854. // TODO: do not draw a circle, but an arc
  12855. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  12856. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  12857. ctx.stroke();
  12858. // draw all arrows
  12859. length = 10 + 5 * this.width; // TODO: make customizable?
  12860. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  12861. ctx.fill();
  12862. ctx.stroke();
  12863. // draw label
  12864. if (this.label) {
  12865. point = this._pointOnCircle(x, y, radius, 0.5);
  12866. this._label(ctx, this.label, point.x, point.y);
  12867. }
  12868. }
  12869. };
  12870. /**
  12871. * Calculate the distance between a point (x3,y3) and a line segment from
  12872. * (x1,y1) to (x2,y2).
  12873. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  12874. * @param {number} x1
  12875. * @param {number} y1
  12876. * @param {number} x2
  12877. * @param {number} y2
  12878. * @param {number} x3
  12879. * @param {number} y3
  12880. * @private
  12881. */
  12882. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  12883. var px = x2-x1,
  12884. py = y2-y1,
  12885. something = px*px + py*py,
  12886. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  12887. if (u > 1) {
  12888. u = 1;
  12889. }
  12890. else if (u < 0) {
  12891. u = 0;
  12892. }
  12893. var x = x1 + u * px,
  12894. y = y1 + u * py,
  12895. dx = x - x3,
  12896. dy = y - y3;
  12897. //# Note: If the actual distance does not matter,
  12898. //# if you only want to compare what this function
  12899. //# returns to other results of this function, you
  12900. //# can just return the squared distance instead
  12901. //# (i.e. remove the sqrt) to gain a little performance
  12902. return Math.sqrt(dx*dx + dy*dy);
  12903. };
  12904. /**
  12905. * This allows the zoom level of the graph to influence the rendering
  12906. *
  12907. * @param scale
  12908. */
  12909. Edge.prototype.setScale = function(scale) {
  12910. this.graphScaleInv = 1.0/scale;
  12911. };
  12912. /**
  12913. * Popup is a class to create a popup window with some text
  12914. * @param {Element} container The container object.
  12915. * @param {Number} [x]
  12916. * @param {Number} [y]
  12917. * @param {String} [text]
  12918. */
  12919. function Popup(container, x, y, text) {
  12920. if (container) {
  12921. this.container = container;
  12922. }
  12923. else {
  12924. this.container = document.body;
  12925. }
  12926. this.x = 0;
  12927. this.y = 0;
  12928. this.padding = 5;
  12929. if (x !== undefined && y !== undefined ) {
  12930. this.setPosition(x, y);
  12931. }
  12932. if (text !== undefined) {
  12933. this.setText(text);
  12934. }
  12935. // create the frame
  12936. this.frame = document.createElement("div");
  12937. var style = this.frame.style;
  12938. style.position = "absolute";
  12939. style.visibility = "hidden";
  12940. style.border = "1px solid #666";
  12941. style.color = "black";
  12942. style.padding = this.padding + "px";
  12943. style.backgroundColor = "#FFFFC6";
  12944. style.borderRadius = "3px";
  12945. style.MozBorderRadius = "3px";
  12946. style.WebkitBorderRadius = "3px";
  12947. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  12948. style.whiteSpace = "nowrap";
  12949. this.container.appendChild(this.frame);
  12950. };
  12951. /**
  12952. * @param {number} x Horizontal position of the popup window
  12953. * @param {number} y Vertical position of the popup window
  12954. */
  12955. Popup.prototype.setPosition = function(x, y) {
  12956. this.x = parseInt(x);
  12957. this.y = parseInt(y);
  12958. };
  12959. /**
  12960. * Set the text for the popup window. This can be HTML code
  12961. * @param {string} text
  12962. */
  12963. Popup.prototype.setText = function(text) {
  12964. this.frame.innerHTML = text;
  12965. };
  12966. /**
  12967. * Show the popup window
  12968. * @param {boolean} show Optional. Show or hide the window
  12969. */
  12970. Popup.prototype.show = function (show) {
  12971. if (show === undefined) {
  12972. show = true;
  12973. }
  12974. if (show) {
  12975. var height = this.frame.clientHeight;
  12976. var width = this.frame.clientWidth;
  12977. var maxHeight = this.frame.parentNode.clientHeight;
  12978. var maxWidth = this.frame.parentNode.clientWidth;
  12979. var top = (this.y - height);
  12980. if (top + height + this.padding > maxHeight) {
  12981. top = maxHeight - height - this.padding;
  12982. }
  12983. if (top < this.padding) {
  12984. top = this.padding;
  12985. }
  12986. var left = this.x;
  12987. if (left + width + this.padding > maxWidth) {
  12988. left = maxWidth - width - this.padding;
  12989. }
  12990. if (left < this.padding) {
  12991. left = this.padding;
  12992. }
  12993. this.frame.style.left = left + "px";
  12994. this.frame.style.top = top + "px";
  12995. this.frame.style.visibility = "visible";
  12996. }
  12997. else {
  12998. this.hide();
  12999. }
  13000. };
  13001. /**
  13002. * Hide the popup window
  13003. */
  13004. Popup.prototype.hide = function () {
  13005. this.frame.style.visibility = "hidden";
  13006. };
  13007. /**
  13008. * @class Groups
  13009. * This class can store groups and properties specific for groups.
  13010. */
  13011. Groups = function () {
  13012. this.clear();
  13013. this.defaultIndex = 0;
  13014. };
  13015. /**
  13016. * default constants for group colors
  13017. */
  13018. Groups.DEFAULT = [
  13019. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  13020. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  13021. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  13022. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  13023. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  13024. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  13025. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  13026. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  13027. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  13028. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  13029. ];
  13030. /**
  13031. * Clear all groups
  13032. */
  13033. Groups.prototype.clear = function () {
  13034. this.groups = {};
  13035. this.groups.length = function()
  13036. {
  13037. var i = 0;
  13038. for ( var p in this ) {
  13039. if (this.hasOwnProperty(p)) {
  13040. i++;
  13041. }
  13042. }
  13043. return i;
  13044. }
  13045. };
  13046. /**
  13047. * get group properties of a groupname. If groupname is not found, a new group
  13048. * is added.
  13049. * @param {*} groupname Can be a number, string, Date, etc.
  13050. * @return {Object} group The created group, containing all group properties
  13051. */
  13052. Groups.prototype.get = function (groupname) {
  13053. var group = this.groups[groupname];
  13054. if (group == undefined) {
  13055. // create new group
  13056. var index = this.defaultIndex % Groups.DEFAULT.length;
  13057. this.defaultIndex++;
  13058. group = {};
  13059. group.color = Groups.DEFAULT[index];
  13060. this.groups[groupname] = group;
  13061. }
  13062. return group;
  13063. };
  13064. /**
  13065. * Add a custom group style
  13066. * @param {String} groupname
  13067. * @param {Object} style An object containing borderColor,
  13068. * backgroundColor, etc.
  13069. * @return {Object} group The created group object
  13070. */
  13071. Groups.prototype.add = function (groupname, style) {
  13072. this.groups[groupname] = style;
  13073. if (style.color) {
  13074. style.color = Node.parseColor(style.color);
  13075. }
  13076. return style;
  13077. };
  13078. /**
  13079. * @class Images
  13080. * This class loads images and keeps them stored.
  13081. */
  13082. Images = function () {
  13083. this.images = {};
  13084. this.callback = undefined;
  13085. };
  13086. /**
  13087. * Set an onload callback function. This will be called each time an image
  13088. * is loaded
  13089. * @param {function} callback
  13090. */
  13091. Images.prototype.setOnloadCallback = function(callback) {
  13092. this.callback = callback;
  13093. };
  13094. /**
  13095. *
  13096. * @param {string} url Url of the image
  13097. * @return {Image} img The image object
  13098. */
  13099. Images.prototype.load = function(url) {
  13100. var img = this.images[url];
  13101. if (img == undefined) {
  13102. // create the image
  13103. var images = this;
  13104. img = new Image();
  13105. this.images[url] = img;
  13106. img.onload = function() {
  13107. if (images.callback) {
  13108. images.callback(this);
  13109. }
  13110. };
  13111. img.src = url;
  13112. }
  13113. return img;
  13114. };
  13115. /**
  13116. * @constructor Cluster
  13117. * Contains the cluster properties for the graph object
  13118. */
  13119. function Cluster() {
  13120. this.clusterSession = 0;
  13121. this.hubThreshold = 5;
  13122. }
  13123. /**
  13124. * This function can be called to open up a specific cluster.
  13125. * It will unpack the cluster back one level.
  13126. *
  13127. * @param node | Node object: cluster to open.
  13128. */
  13129. Cluster.prototype.openCluster = function(node) {
  13130. var isMovingBeforeClustering = this.moving;
  13131. this._expandClusterNode(node,false,true);
  13132. // housekeeping
  13133. this._updateNodeIndexList();
  13134. this._updateDynamicEdges();
  13135. this._updateLabels();
  13136. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  13137. if (this.moving != isMovingBeforeClustering) {
  13138. this.start();
  13139. }
  13140. };
  13141. /**
  13142. * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
  13143. * be clustered with their connected node. This can be repeated as many times as needed.
  13144. * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
  13145. */
  13146. Cluster.prototype.increaseClusterLevel = function() {
  13147. this.updateClusters(-1,false,true);
  13148. };
  13149. /**
  13150. * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
  13151. * be unpacked if they are a cluster. This can be repeated as many times as needed.
  13152. * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
  13153. */
  13154. Cluster.prototype.decreaseClusterLevel = function() {
  13155. this.updateClusters(1,false,true);
  13156. };
  13157. /**
  13158. * This function clusters on zoom, it can be called with a predefined zoom direction
  13159. * If out, check if we can form clusters, if in, check if we can open clusters.
  13160. * This function is only called from _zoom()
  13161. *
  13162. * @param {Int} zoomDirection
  13163. * @param {Boolean} recursive | enable or disable recursive calling of the opening of clusters
  13164. * @param {Boolean} force | enable or disable forcing
  13165. *
  13166. * @private
  13167. */
  13168. Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
  13169. var isMovingBeforeClustering = this.moving;
  13170. var amountOfNodes = this.nodeIndices.length;
  13171. // check if we zoom in or out
  13172. if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
  13173. this._formClusters(force);
  13174. }
  13175. else if (this.previousScale < this.scale || zoomDirection == 1) { // zoom out
  13176. this._openClusters(recursive,force);
  13177. }
  13178. this._updateNodeIndexList();
  13179. // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs and update the index again
  13180. if (this.nodeIndices.length == amountOfNodes && (this.previousScale > this.scale || zoomDirection == -1)) {
  13181. this._aggregateHubs(force);
  13182. this._updateNodeIndexList();
  13183. }
  13184. this.previousScale = this.scale;
  13185. // rest of the housekeeping
  13186. this._updateDynamicEdges();
  13187. this._updateLabels();
  13188. // if a cluster was formed, we increase the clusterSession
  13189. if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
  13190. this.clusterSession += 1;
  13191. }
  13192. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  13193. if (this.moving != isMovingBeforeClustering) {
  13194. this.start();
  13195. }
  13196. };
  13197. /**
  13198. * this functions starts clustering by hubs
  13199. * The minimum hub threshold is set globally
  13200. *
  13201. * @private
  13202. */
  13203. Cluster.prototype._aggregateHubs = function(force) {
  13204. this._getHubSize();
  13205. this._clusterByHub(force);
  13206. };
  13207. /**
  13208. * This function is fired by keypress. It forces hubs to form.
  13209. *
  13210. */
  13211. Cluster.prototype.forceAggregateHubs = function() {
  13212. var isMovingBeforeClustering = this.moving;
  13213. var amountOfNodes = this.nodeIndices.length;
  13214. this._aggregateHubs(true);
  13215. // housekeeping
  13216. this._updateNodeIndexList();
  13217. this._updateDynamicEdges();
  13218. this._updateLabels();
  13219. // if a cluster was formed, we increase the clusterSession
  13220. if (this.nodeIndices.length != amountOfNodes) {
  13221. this.clusterSession += 1;
  13222. }
  13223. // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
  13224. if (this.moving != isMovingBeforeClustering) {
  13225. this.start();
  13226. }
  13227. };
  13228. /**
  13229. * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
  13230. * has to be opened based on the current zoom level.
  13231. *
  13232. * @private
  13233. */
  13234. Cluster.prototype._openClusters = function(recursive,force) {
  13235. for (var i = 0; i < this.nodeIndices.length; i++) {
  13236. var node = this.nodes[this.nodeIndices[i]];
  13237. this._expandClusterNode(node,recursive,force);
  13238. }
  13239. };
  13240. /**
  13241. * This function checks if a node has to be opened. This is done by checking the zoom level.
  13242. * If the node contains child nodes, this function is recursively called on the child nodes as well.
  13243. * This recursive behaviour is optional and can be set by the recursive argument.
  13244. *
  13245. * @param {Node} parentNode | to check for cluster and expand
  13246. * @param {Boolean} recursive | enable or disable recursive calling
  13247. * @param {Boolean} force | enable or disable forcing
  13248. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  13249. * @private
  13250. */
  13251. Cluster.prototype._expandClusterNode = function(parentNode, recursive, force, openAll) {
  13252. var openedCluster = false;
  13253. // first check if node is a cluster
  13254. if (parentNode.clusterSize > 1) {
  13255. // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
  13256. if (parentNode.clusterSize < 20 && force == false) {
  13257. openAll = true;
  13258. }
  13259. recursive = openAll ? true : recursive;
  13260. // if the last child has been added on a smaller scale than current scale (@optimization)
  13261. if (parentNode.formationScale < this.scale || force == true) {
  13262. // we will check if any of the contained child nodes should be removed from the cluster
  13263. for (var containedNodeID in parentNode.containedNodes) {
  13264. if (parentNode.containedNodes.hasOwnProperty(containedNodeID)) {
  13265. var childNode = parentNode.containedNodes[containedNodeID];
  13266. // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
  13267. // the largest cluster is the one that comes from outside
  13268. if (force == true) {
  13269. if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
  13270. || openAll) {
  13271. this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
  13272. openedCluster = true;
  13273. }
  13274. }
  13275. else {
  13276. if (this._parentNodeInActiveArea(parentNode)) {
  13277. this._expelChildFromParent(parentNode,containedNodeID,recursive,force,openAll);
  13278. openedCluster = true;
  13279. }
  13280. }
  13281. }
  13282. }
  13283. }
  13284. if (openedCluster == true) {
  13285. parentNode.clusterSessions.pop();
  13286. }
  13287. }
  13288. };
  13289. /**
  13290. * ONLY CALLED FROM _expandClusterNode
  13291. *
  13292. * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
  13293. * the child node from the parent contained_node object and put it back into the global nodes object.
  13294. * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
  13295. *
  13296. * @param {Node} parentNode | the parent node
  13297. * @param {String} containedNodeID | child_node id as it is contained in the containedNodes object of the parent node
  13298. * @param {Boolean} recursive | This will also check if the child needs to be expanded.
  13299. * With force and recursive both true, the entire cluster is unpacked
  13300. * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
  13301. * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
  13302. * @private
  13303. */
  13304. Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, force, openAll) {
  13305. var childNode = parentNode.containedNodes[containedNodeID];
  13306. // if child node has been added on smaller scale than current, kick out
  13307. if (childNode.formationScale < this.scale || force == true) {
  13308. // put the child node back in the global nodes object
  13309. this.nodes[containedNodeID] = childNode;
  13310. // release the contained edges from this childNode back into the global edges
  13311. this._releaseContainedEdges(parentNode,childNode);
  13312. // reconnect rerouted edges to the childNode
  13313. this._connectEdgeBackToChild(parentNode,childNode);
  13314. // validate all edges in dynamicEdges
  13315. this._validateEdges(parentNode);
  13316. // undo the changes from the clustering operation on the parent node
  13317. parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
  13318. parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
  13319. parentNode.clusterSize -= childNode.clusterSize;
  13320. parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
  13321. // place the child node near the parent, not at the exact same location to avoid chaos in the system
  13322. childNode.x = parentNode.x + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
  13323. childNode.y = parentNode.y + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
  13324. // remove the clusterSession from the child node
  13325. childNode.clusterSession = 0;
  13326. // remove node from the list
  13327. delete parentNode.containedNodes[containedNodeID];
  13328. // restart the simulation to reorganise all nodes
  13329. this.moving = true;
  13330. // recalculate the size of the node on the next time the node is rendered
  13331. parentNode.clearSizeCache();
  13332. }
  13333. // check if a further expansion step is possible if recursivity is enabled
  13334. if (recursive == true) {
  13335. this._expandClusterNode(childNode,recursive,force,openAll);
  13336. }
  13337. };
  13338. /**
  13339. * This function checks if any nodes at the end of their trees have edges below a threshold length
  13340. * This function is called only from updateClusters()
  13341. * forceLevelCollapse ignores the length of the edge and collapses one level
  13342. * This means that a node with only one edge will be clustered with its connected node
  13343. *
  13344. * @private
  13345. * @param {Boolean} force
  13346. */
  13347. Cluster.prototype._formClusters = function(force) {
  13348. if (force == false) {
  13349. this._formClustersByZoom();
  13350. }
  13351. else {
  13352. this._forceClustersByZoom();
  13353. }
  13354. };
  13355. /**
  13356. * This function handles the clustering by zooming out, this is based on a minimum edge distance
  13357. *
  13358. * @private
  13359. */
  13360. Cluster.prototype._formClustersByZoom = function() {
  13361. var dx,dy,length,
  13362. minLength = this.constants.clustering.clusterLength/this.scale;
  13363. // check if any edges are shorter than minLength and start the clustering
  13364. // the clustering favours the node with the larger mass
  13365. for (var edgeID in this.edges) {
  13366. if (this.edges.hasOwnProperty(edgeID)) {
  13367. var edge = this.edges[edgeID];
  13368. if (edge.connected) {
  13369. dx = (edge.to.x - edge.from.x);
  13370. dy = (edge.to.y - edge.from.y);
  13371. length = Math.sqrt(dx * dx + dy * dy);
  13372. if (length < minLength) {
  13373. // first check which node is larger
  13374. var parentNode = edge.from;
  13375. var childNode = edge.to;
  13376. if (edge.to.mass > edge.from.mass) {
  13377. parentNode = edge.to;
  13378. childNode = edge.from;
  13379. }
  13380. if (childNode.dynamicEdgesLength == 1) {
  13381. this._addToCluster(parentNode,childNode,false);
  13382. }
  13383. else if (parentNode.dynamicEdgesLength == 1) {
  13384. this._addToCluster(childNode,parentNode,false);
  13385. }
  13386. }
  13387. }
  13388. }
  13389. }
  13390. };
  13391. /**
  13392. * This function forces the graph to cluster all nodes with only one connecting edge to their
  13393. * connected node.
  13394. *
  13395. * @private
  13396. */
  13397. Cluster.prototype._forceClustersByZoom = function() {
  13398. for (var nodeID in this.nodes) {
  13399. // another node could have absorbed this child.
  13400. if (this.nodes.hasOwnProperty(nodeID)) {
  13401. var childNode = this.nodes[nodeID];
  13402. // the edges can be swallowed by another decrease
  13403. if (childNode.dynamicEdgesLength == 1 && childNode.dynamicEdges.length != 0) {
  13404. var edge = childNode.dynamicEdges[0];
  13405. var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
  13406. // group to the largest node
  13407. if (parentNode.mass > childNode.mass) {
  13408. this._addToCluster(parentNode,childNode,true);
  13409. }
  13410. else {
  13411. this._addToCluster(childNode,parentNode,true);
  13412. }
  13413. }
  13414. }
  13415. }
  13416. };
  13417. /**
  13418. *
  13419. * @param {Boolean} force
  13420. * @private
  13421. */
  13422. Cluster.prototype._clusterByHub = function(force) {
  13423. var dx,dy,length;
  13424. var minLength = this.constants.clustering.clusterLength/this.scale;
  13425. var allowCluster = false;
  13426. // we loop over all nodes in the list
  13427. for (var nodeID in this.nodes) {
  13428. // we check if it is still available since it can be used by the clustering in this loop
  13429. if (this.nodes.hasOwnProperty(nodeID)) {
  13430. var hubNode = this.nodes[nodeID];
  13431. // we decide if the node is a hub
  13432. if (hubNode.dynamicEdgesLength >= this.hubThreshold) {
  13433. // we create a list of edges because the dynamicEdges change over the course of this loop
  13434. var edgesIDarray = [];
  13435. var amountOfInitialEdges = hubNode.dynamicEdges.length;
  13436. for (var j = 0; j < amountOfInitialEdges; j++) {
  13437. edgesIDarray.push(hubNode.dynamicEdges[j].id);
  13438. }
  13439. // if the hub clustering is not forces, we check if one of the edges connected
  13440. // to a cluster is small enough based on the constants.clustering.clusterLength
  13441. if (force == false) {
  13442. allowCluster = false;
  13443. for (j = 0; j < amountOfInitialEdges; j++) {
  13444. var edge = this.edges[edgesIDarray[j]];
  13445. if (edge !== undefined) {
  13446. if (edge.connected) {
  13447. dx = (edge.to.x - edge.from.x);
  13448. dy = (edge.to.y - edge.from.y);
  13449. length = Math.sqrt(dx * dx + dy * dy);
  13450. if (length < minLength) {
  13451. allowCluster = true;
  13452. break;
  13453. }
  13454. }
  13455. }
  13456. }
  13457. }
  13458. // start the clustering if allowed
  13459. if ((!force && allowCluster) || force) {
  13460. // we loop over all edges INITIALLY connected to this hub
  13461. for (j = 0; j < amountOfInitialEdges; j++) {
  13462. edge = this.edges[edgesIDarray[j]];
  13463. // the edge can be clustered by this function in a previous loop
  13464. if (edge !== undefined) {
  13465. var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
  13466. // we do not want hubs to merge with other hubs.
  13467. if (childNode.dynamicEdges.length <= this.hubThreshold) {
  13468. this._addToCluster(hubNode,childNode,force);
  13469. }
  13470. }
  13471. }
  13472. }
  13473. }
  13474. }
  13475. }
  13476. };
  13477. /**
  13478. * This function adds the child node to the parent node, creating a cluster if it is not already.
  13479. * This function is called only from updateClusters()
  13480. *
  13481. * @param {Node} parentNode | this is the node that will house the child node
  13482. * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
  13483. * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
  13484. * @private
  13485. */
  13486. Cluster.prototype._addToCluster = function(parentNode, childNode, force) {
  13487. // join child node in the parent node
  13488. parentNode.containedNodes[childNode.id] = childNode;
  13489. // manage all the edges connected to the child and parent nodes
  13490. for (var i = 0; i < childNode.dynamicEdges.length; i++) {
  13491. var edge = childNode.dynamicEdges[i];
  13492. if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
  13493. this._addToContainedEdges(parentNode,childNode,edge);
  13494. }
  13495. else {
  13496. this._connectEdgeToCluster(parentNode,childNode,edge);
  13497. }
  13498. }
  13499. childNode.dynamicEdges = [];
  13500. // remove the childNode from the global nodes object
  13501. delete this.nodes[childNode.id];
  13502. var massBefore = parentNode.mass;
  13503. childNode.clusterSession = this.clusterSession;
  13504. parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
  13505. parentNode.clusterSize += childNode.clusterSize;
  13506. parentNode.fontSize += this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
  13507. // keep track of the clustersessions so we can open the cluster up as it has been formed.
  13508. if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
  13509. parentNode.clusterSessions.push(this.clusterSession);
  13510. }
  13511. // giving the clusters a dynamic formationScale to ensure not all clusters open up when zoomed
  13512. if (force == true) {
  13513. parentNode.formationScale = Math.pow(1 - (1.0/11.0),this.clusterSession+2);
  13514. }
  13515. else {
  13516. parentNode.formationScale = this.scale; // The latest child has been added on this scale
  13517. }
  13518. // recalculate the size of the node on the next time the node is rendered
  13519. parentNode.clearSizeCache();
  13520. // set the pop-out scale for the childnode
  13521. parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
  13522. // nullify the movement velocity of the child, this is to avoid hectic behaviour
  13523. childNode.clearVelocity();
  13524. // the mass has altered, preservation of energy dictates the velocity to be updated
  13525. parentNode.updateVelocity(massBefore);
  13526. // restart the simulation to reorganise all nodes
  13527. this.moving = true;
  13528. };
  13529. /**
  13530. * This function will apply the changes made to the remainingEdges during the formation of the clusters.
  13531. * This is a seperate function to allow for level-wise collapsing of the node tree.
  13532. * It has to be called if a level is collapsed. It is called by _formClusters().
  13533. * @private
  13534. */
  13535. Cluster.prototype._updateDynamicEdges = function() {
  13536. for (var i = 0; i < this.nodeIndices.length; i++) {
  13537. var node = this.nodes[this.nodeIndices[i]];
  13538. node.dynamicEdgesLength = node.dynamicEdges.length;
  13539. // this corrects for multiple edges pointing at the same other node
  13540. var correction = 0;
  13541. if (node.dynamicEdgesLength > 1) {
  13542. for (var j = 0; j < node.dynamicEdgesLength - 1; j++) {
  13543. var edgeToId = node.dynamicEdges[j].toId;
  13544. var edgeFromId = node.dynamicEdges[j].fromId;
  13545. for (var k = j+1; k < node.dynamicEdgesLength; k++) {
  13546. if ((node.dynamicEdges[k].toId == edgeToId && node.dynamicEdges[k].fromId == edgeFromId) ||
  13547. (node.dynamicEdges[k].fromId == edgeToId && node.dynamicEdges[k].toId == edgeFromId)) {
  13548. correction += 1;
  13549. }
  13550. }
  13551. }
  13552. }
  13553. node.dynamicEdgesLength -= correction;
  13554. }
  13555. };
  13556. /**
  13557. * This adds an edge from the childNode to the contained edges of the parent node
  13558. *
  13559. * @param parentNode | Node object
  13560. * @param childNode | Node object
  13561. * @param edge | Edge object
  13562. * @private
  13563. */
  13564. Cluster.prototype._addToContainedEdges = function(parentNode, childNode, edge) {
  13565. // create an array object if it does not yet exist for this childNode
  13566. if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
  13567. parentNode.containedEdges[childNode.id] = []
  13568. }
  13569. // add this edge to the list
  13570. parentNode.containedEdges[childNode.id].push(edge);
  13571. // remove the edge from the global edges object
  13572. delete this.edges[edge.id];
  13573. // remove the edge from the parent object
  13574. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  13575. if (parentNode.dynamicEdges[i].id == edge.id) {
  13576. parentNode.dynamicEdges.splice(i,1);
  13577. break;
  13578. }
  13579. }
  13580. };
  13581. /**
  13582. * This function connects an edge that was connected to a child node to the parent node.
  13583. * It keeps track of which nodes it has been connected to with the originalID array.
  13584. *
  13585. * @param parentNode | Node object
  13586. * @param childNode | Node object
  13587. * @param edge | Edge object
  13588. * @private
  13589. */
  13590. Cluster.prototype._connectEdgeToCluster = function(parentNode, childNode, edge) {
  13591. if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
  13592. edge.originalToID.push(childNode.id);
  13593. edge.to = parentNode;
  13594. edge.toId = parentNode.id;
  13595. }
  13596. else { // edge connected to other node with the "from" side
  13597. edge.originalFromID.push(childNode.id);
  13598. edge.from = parentNode;
  13599. edge.fromId = parentNode.id;
  13600. }
  13601. this._addToReroutedEdges(parentNode,childNode,edge);
  13602. };
  13603. /**
  13604. * This adds an edge from the childNode to the rerouted edges of the parent node
  13605. *
  13606. * @param parentNode | Node object
  13607. * @param childNode | Node object
  13608. * @param edge | Edge object
  13609. * @private
  13610. */
  13611. Cluster.prototype._addToReroutedEdges = function(parentNode, childNode, edge) {
  13612. // create an array object if it does not yet exist for this childNode
  13613. // we store the edge in the rerouted edges so we can restore it when the cluster pops open
  13614. if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
  13615. parentNode.reroutedEdges[childNode.id] = [];
  13616. }
  13617. parentNode.reroutedEdges[childNode.id].push(edge);
  13618. // this edge becomes part of the dynamicEdges of the cluster node
  13619. parentNode.dynamicEdges.push(edge);
  13620. };
  13621. /**
  13622. * This function connects an edge that was connected to a cluster node back to the child node.
  13623. *
  13624. * @param parentNode | Node object
  13625. * @param childNode | Node object
  13626. * @private
  13627. */
  13628. Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
  13629. if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
  13630. for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
  13631. var edge = parentNode.reroutedEdges[childNode.id][i];
  13632. if (edge.originalFromID[edge.originalFromID.length-1] == childNode.id) {
  13633. edge.originalFromID.pop();
  13634. edge.fromId = childNode.id;
  13635. edge.from = childNode;
  13636. }
  13637. else {
  13638. edge.originalToID.pop();
  13639. edge.toId = childNode.id;
  13640. edge.to = childNode;
  13641. }
  13642. // append this edge to the list of edges connecting to the childnode
  13643. childNode.dynamicEdges.push(edge);
  13644. // remove the edge from the parent object
  13645. for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
  13646. if (parentNode.dynamicEdges[j].id == edge.id) {
  13647. parentNode.dynamicEdges.splice(j,1);
  13648. break;
  13649. }
  13650. }
  13651. }
  13652. // remove the entry from the rerouted edges
  13653. delete parentNode.reroutedEdges[childNode.id];
  13654. }
  13655. };
  13656. /**
  13657. * When loops are clustered, an edge can be both in the rerouted array and the contained array.
  13658. * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
  13659. * parentNode
  13660. *
  13661. * @param parentNode | Node object
  13662. * @private
  13663. */
  13664. Cluster.prototype._validateEdges = function(parentNode) {
  13665. // TODO: check if good idea
  13666. for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
  13667. var edge = parentNode.dynamicEdges[i];
  13668. if (parentNode.id != edge.toId && parentNode.id != edge.fromId) {
  13669. parentNode.dynamicEdges.splice(i,1);
  13670. }
  13671. }
  13672. };
  13673. /**
  13674. * This function released the contained edges back into the global domain and puts them back into the
  13675. * dynamic edges of both parent and child.
  13676. *
  13677. * @param {Node} parentNode |
  13678. * @param {Node} childNode |
  13679. * @private
  13680. */
  13681. Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) {
  13682. for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
  13683. var edge = parentNode.containedEdges[childNode.id][i];
  13684. // put the edge back in the global edges object
  13685. this.edges[edge.id] = edge;
  13686. // put the edge back in the dynamic edges of the child and parent
  13687. childNode.dynamicEdges.push(edge);
  13688. parentNode.dynamicEdges.push(edge);
  13689. }
  13690. // remove the entry from the contained edges
  13691. delete parentNode.containedEdges[childNode.id];
  13692. };
  13693. // ------------------- UTILITY FUNCTIONS ---------------------------- //
  13694. /**
  13695. * This updates the node labels for all nodes (for debugging purposes)
  13696. * @private
  13697. */
  13698. Cluster.prototype._updateLabels = function() {
  13699. var nodeID;
  13700. // update node labels
  13701. for (nodeID in this.nodes) {
  13702. if (this.nodes.hasOwnProperty(nodeID)) {
  13703. var node = this.nodes[nodeID];
  13704. if (node.clusterSize > 1) {
  13705. node.label = "[".concat(String(node.clusterSize),"]");
  13706. }
  13707. }
  13708. }
  13709. // update node labels
  13710. for (nodeID in this.nodes) {
  13711. if (this.nodes.hasOwnProperty(nodeID)) {
  13712. node = this.nodes[nodeID];
  13713. if (node.clusterSize == 1) {
  13714. node.label = String(node.id);
  13715. }
  13716. }
  13717. }
  13718. /* Debug Override */
  13719. for (nodeID in this.nodes) {
  13720. if (this.nodes.hasOwnProperty(nodeID)) {
  13721. node = this.nodes[nodeID];
  13722. node.label = String(node.clusterSize).concat(":",String(node.id));
  13723. }
  13724. }
  13725. };
  13726. /**
  13727. * This function determines if the cluster we want to decluster is in the active area
  13728. * this means around the zoom center
  13729. *
  13730. * @param {Node} node
  13731. * @returns {boolean}
  13732. * @private
  13733. */
  13734. Cluster.prototype._parentNodeInActiveArea = function(node) {
  13735. return (
  13736. Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13737. &&
  13738. Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
  13739. )
  13740. };
  13741. /**
  13742. * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
  13743. * It puts large clusters away from the center and randomizes the order.
  13744. *
  13745. * @private
  13746. */
  13747. Cluster.prototype._repositionNodes = function() {
  13748. for (var i = 0; i < this.nodeIndices.length; i++) {
  13749. var node = this.nodes[this.nodeIndices[i]];
  13750. if (!node.isFixed()) {
  13751. var radius = this.constants.edges.length * (1 + 0.6*node.clusterSize);
  13752. var angle = 2 * Math.PI * Math.random();
  13753. node.x = radius * Math.cos(angle);
  13754. node.y = radius * Math.sin(angle);
  13755. }
  13756. }
  13757. };
  13758. /**
  13759. * We determine how many connections denote an important hub.
  13760. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
  13761. *
  13762. * @private
  13763. */
  13764. Cluster.prototype._getHubSize = function() {
  13765. var average = 0;
  13766. var averageSquared = 0;
  13767. var hubCounter = 0;
  13768. var largestHub = 0;
  13769. for (var i = 0; i < this.nodeIndices.length; i++) {
  13770. var node = this.nodes[this.nodeIndices[i]];
  13771. if (node.dynamicEdgesLength > largestHub) {
  13772. largestHub = node.dynamicEdgesLength;
  13773. }
  13774. average += node.dynamicEdgesLength;
  13775. averageSquared += Math.pow(node.dynamicEdgesLength,2);
  13776. hubCounter += 1;
  13777. }
  13778. average = average / hubCounter;
  13779. averageSquared = averageSquared / hubCounter;
  13780. var variance = averageSquared - Math.pow(average,2);
  13781. var standardDeviation = Math.sqrt(variance);
  13782. this.hubThreshold = Math.floor(average + 2*standardDeviation);
  13783. // always have at least one to cluster
  13784. if (this.hubThreshold > largestHub) {
  13785. this.hubThreshold = largestHub;
  13786. }
  13787. // console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
  13788. // console.log("hubThreshold:",this.hubThreshold);
  13789. };
  13790. /**
  13791. * We get the amount of "extension nodes" or snakes. These are not quickly clustered with the outliers and hubs methods
  13792. * with this amount we can cluster specifically on these snakes.
  13793. *
  13794. * @returns {number}
  13795. * @private
  13796. */
  13797. Cluster.prototype._getAmountOfSnakes = function() {
  13798. var snakes = 0;
  13799. for (nodeID in this.nodes) {
  13800. if (this.nodes.hasOwnProperty(nodeID)) {
  13801. if (this.nodes[nodeID].dynamicEdges.length == 2) {
  13802. snakes += 1;
  13803. }
  13804. }
  13805. }
  13806. return snakes;
  13807. };
  13808. /**
  13809. * @constructor Graph
  13810. * Create a graph visualization, displaying nodes and edges.
  13811. *
  13812. * @param {Element} container The DOM element in which the Graph will
  13813. * be created. Normally a div element.
  13814. * @param {Object} data An object containing parameters
  13815. * {Array} nodes
  13816. * {Array} edges
  13817. * @param {Object} options Options
  13818. */
  13819. function Graph (container, data, options) {
  13820. // create variables and set default values
  13821. this.containerElement = container;
  13822. this.width = '100%';
  13823. this.height = '100%';
  13824. this.refreshRate = 50; // milliseconds
  13825. this.stabilize = true; // stabilize before displaying the graph
  13826. this.selectable = true;
  13827. // set constant values
  13828. this.constants = {
  13829. nodes: {
  13830. radiusMin: 5,
  13831. radiusMax: 20,
  13832. radius: 5,
  13833. distance: 100, // px
  13834. shape: 'ellipse',
  13835. image: undefined,
  13836. widthMin: 16, // px
  13837. widthMax: 64, // px
  13838. fontColor: 'black',
  13839. fontSize: 14, // px
  13840. //fontFace: verdana,
  13841. fontFace: 'arial',
  13842. color: {
  13843. border: '#2B7CE9',
  13844. background: '#97C2FC',
  13845. highlight: {
  13846. border: '#2B7CE9',
  13847. background: '#D2E5FF'
  13848. }
  13849. },
  13850. borderColor: '#2B7CE9',
  13851. backgroundColor: '#97C2FC',
  13852. highlightColor: '#D2E5FF',
  13853. group: undefined
  13854. },
  13855. edges: {
  13856. widthMin: 1,
  13857. widthMax: 15,
  13858. width: 1,
  13859. style: 'line',
  13860. color: '#343434',
  13861. fontColor: '#343434',
  13862. fontSize: 14, // px
  13863. fontFace: 'arial',
  13864. //distance: 100, //px
  13865. length: 100, // px
  13866. dash: {
  13867. length: 10,
  13868. gap: 5,
  13869. altLength: undefined
  13870. }
  13871. },
  13872. clustering: { // TODO: naming of variables
  13873. maxNumberOfNodes: 100, // for automatic (initial) clustering //
  13874. clusterLength: 30, // threshold edge length for clusteringl
  13875. fontSizeMultiplier: 3, // how much the cluster font size grows per node (in px)
  13876. forceAmplification: 0.7, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
  13877. distanceAmplification: 0.3, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
  13878. edgeStrength: 0.01,
  13879. edgeGrowth: 11, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
  13880. clusterSizeWidthFactor: 10,
  13881. clusterSizeHeightFactor: 10,
  13882. clusterSizeRadiusFactor: 10,
  13883. activeAreaBoxSize: 100, // box area around the curser where clusters are popped open
  13884. massTransferCoefficient: 1 // parent.mass += massTransferCoefficient * child.mass
  13885. },
  13886. minForce: 0.05,
  13887. minVelocity: 0.02, // px/s
  13888. maxIterations: 1000 // maximum number of iteration to stabilize
  13889. };
  13890. // call the constructor of the cluster object
  13891. Cluster.call(this);
  13892. var graph = this;
  13893. this.freezeSimulation = false;
  13894. this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
  13895. this.tapTimer = 0;
  13896. this.pocketUniverse = {};
  13897. this.nodes = {}; // object with Node objects
  13898. this.edges = {}; // object with Edge objects
  13899. this.zoomCenter = {}; // object with x and y elements used for determining the center of the zoom action
  13900. this.scale = 1; // defining the global scale variable in the constructor
  13901. this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
  13902. // TODO: create a counter to keep track on the number of nodes having values
  13903. // TODO: create a counter to keep track on the number of nodes currently moving
  13904. // TODO: create a counter to keep track on the number of edges having values
  13905. this.nodesData = null; // A DataSet or DataView
  13906. this.edgesData = null; // A DataSet or DataView
  13907. // create event listeners used to subscribe on the DataSets of the nodes and edges
  13908. var me = this;
  13909. this.nodesListeners = {
  13910. 'add': function (event, params) {
  13911. me._addNodes(params.items);
  13912. me.start();
  13913. },
  13914. 'update': function (event, params) {
  13915. me._updateNodes(params.items);
  13916. me.start();
  13917. },
  13918. 'remove': function (event, params) {
  13919. me._removeNodes(params.items);
  13920. me.start();
  13921. }
  13922. };
  13923. this.edgesListeners = {
  13924. 'add': function (event, params) {
  13925. me._addEdges(params.items);
  13926. me.start();
  13927. },
  13928. 'update': function (event, params) {
  13929. me._updateEdges(params.items);
  13930. me.start();
  13931. },
  13932. 'remove': function (event, params) {
  13933. me._removeEdges(params.items);
  13934. me.start();
  13935. }
  13936. };
  13937. this.groups = new Groups(); // object with groups
  13938. this.images = new Images(); // object with images
  13939. this.images.setOnloadCallback(function () {
  13940. graph._redraw();
  13941. });
  13942. // properties of the data
  13943. this.moving = false; // True if any of the nodes have an undefined position
  13944. this.selection = [];
  13945. this.timer = undefined;
  13946. // create a frame and canvas
  13947. this._create();
  13948. // apply options
  13949. this.setOptions(options);
  13950. // draw data
  13951. this.setData(data); // TODO: option to render (start())
  13952. // zoom so all data will fit on the screen
  13953. this.zoomToFit();
  13954. // cluster if the data set is big
  13955. this.clusterToFit(true);
  13956. // updates the lables after clustering
  13957. this._updateLabels();
  13958. // find a stable position or start animating to a stable position
  13959. if (this.stabilize) {
  13960. this._doStabilize();
  13961. }
  13962. this.start();
  13963. }
  13964. /**
  13965. * We add the functionality of the cluster object to the graph object
  13966. * @type {Cluster.prototype}
  13967. */
  13968. Graph.prototype = Object.create(Cluster.prototype);
  13969. /**
  13970. * This function clusters until the maxNumberOfNodes has been reached
  13971. *
  13972. * @param {Boolean} reposition
  13973. */
  13974. Graph.prototype.clusterToFit = function(reposition) {
  13975. var numberOfNodes = this.nodeIndices.length;
  13976. var maxNumberOfNodes = this.constants.clustering.maxNumberOfNodes;
  13977. var maxLevels = 10;
  13978. var level = 0;
  13979. // we first cluster the hubs, then we pull in the outliers, repeat
  13980. while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) {
  13981. if (level % 5 == 0) {
  13982. console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession);
  13983. this.forceAggregateHubs();
  13984. }
  13985. else {
  13986. console.log("Pulling in Outliers @ level: ",level,"clusterSession",this.clusterSession);
  13987. this.increaseClusterLevel();
  13988. }
  13989. numberOfNodes = this.nodeIndices.length;
  13990. level += 1;
  13991. }
  13992. // after the clustering we reposition the nodes to avoid initial chaos
  13993. if (level > 1 && reposition == true) {
  13994. this._repositionNodes();
  13995. }
  13996. };
  13997. /**
  13998. * This function zooms out to fit all data on screen based on amount of nodes
  13999. */
  14000. Graph.prototype.zoomToFit = function() {
  14001. var numberOfNodes = this.nodeIndices.length;
  14002. var zoomLevel = 105 / (numberOfNodes + 80); // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
  14003. if (zoomLevel > 1.0) {
  14004. zoomLevel = 1.0;
  14005. }
  14006. if (!('mousewheelScale' in this.pinch)) {
  14007. this.pinch.mousewheelScale = zoomLevel;
  14008. }
  14009. this._setScale(zoomLevel);
  14010. };
  14011. /**
  14012. * Update the this.nodeIndices with the most recent node index list
  14013. * @private
  14014. */
  14015. Graph.prototype._updateNodeIndexList = function() {
  14016. this.nodeIndices = [];
  14017. for (var idx in this.nodes) {
  14018. if (this.nodes.hasOwnProperty(idx)) {
  14019. this.nodeIndices.push(idx);
  14020. }
  14021. }
  14022. };
  14023. /**
  14024. * Set nodes and edges, and optionally options as well.
  14025. *
  14026. * @param {Object} data Object containing parameters:
  14027. * {Array | DataSet | DataView} [nodes] Array with nodes
  14028. * {Array | DataSet | DataView} [edges] Array with edges
  14029. * {String} [dot] String containing data in DOT format
  14030. * {Options} [options] Object with options
  14031. */
  14032. Graph.prototype.setData = function(data) {
  14033. if (data && data.dot && (data.nodes || data.edges)) {
  14034. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  14035. ' parameter pair "nodes" and "edges", but not both.');
  14036. }
  14037. // set options
  14038. this.setOptions(data && data.options);
  14039. // set all data
  14040. if (data && data.dot) {
  14041. // parse DOT file
  14042. if(data && data.dot) {
  14043. var dotData = vis.util.DOTToGraph(data.dot);
  14044. this.setData(dotData);
  14045. return;
  14046. }
  14047. }
  14048. else {
  14049. this._setNodes(data && data.nodes);
  14050. this._setEdges(data && data.edges);
  14051. }
  14052. // updating the list of node indices
  14053. };
  14054. /**
  14055. * Set options
  14056. * @param {Object} options
  14057. */
  14058. Graph.prototype.setOptions = function (options) {
  14059. if (options) {
  14060. // retrieve parameter values
  14061. if (options.width !== undefined) {this.width = options.width;}
  14062. if (options.height !== undefined) {this.height = options.height;}
  14063. if (options.stabilize !== undefined) {this.stabilize = options.stabilize;}
  14064. if (options.selectable !== undefined) {this.selectable = options.selectable;}
  14065. // TODO: work out these options and document them
  14066. if (options.edges) {
  14067. for (var prop in options.edges) {
  14068. if (options.edges.hasOwnProperty(prop)) {
  14069. this.constants.edges[prop] = options.edges[prop];
  14070. }
  14071. }
  14072. if (options.edges.length !== undefined &&
  14073. options.nodes && options.nodes.distance === undefined) {
  14074. this.constants.edges.length = options.edges.length;
  14075. this.constants.nodes.distance = options.edges.length * 1.25;
  14076. }
  14077. if (!options.edges.fontColor) {
  14078. this.constants.edges.fontColor = options.edges.color;
  14079. }
  14080. // Added to support dashed lines
  14081. // David Jordan
  14082. // 2012-08-08
  14083. if (options.edges.dash) {
  14084. if (options.edges.dash.length !== undefined) {
  14085. this.constants.edges.dash.length = options.edges.dash.length;
  14086. }
  14087. if (options.edges.dash.gap !== undefined) {
  14088. this.constants.edges.dash.gap = options.edges.dash.gap;
  14089. }
  14090. if (options.edges.dash.altLength !== undefined) {
  14091. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  14092. }
  14093. }
  14094. }
  14095. if (options.nodes) {
  14096. for (prop in options.nodes) {
  14097. if (options.nodes.hasOwnProperty(prop)) {
  14098. this.constants.nodes[prop] = options.nodes[prop];
  14099. }
  14100. }
  14101. if (options.nodes.color) {
  14102. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  14103. }
  14104. /*
  14105. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  14106. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  14107. */
  14108. }
  14109. if (options.groups) {
  14110. for (var groupname in options.groups) {
  14111. if (options.groups.hasOwnProperty(groupname)) {
  14112. var group = options.groups[groupname];
  14113. this.groups.add(groupname, group);
  14114. }
  14115. }
  14116. }
  14117. }
  14118. this.setSize(this.width, this.height);
  14119. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  14120. this._setScale(1);
  14121. };
  14122. /**
  14123. * fire an event
  14124. * @param {String} event The name of an event, for example 'select'
  14125. * @param {Object} params Optional object with event parameters
  14126. * @private
  14127. */
  14128. Graph.prototype._trigger = function (event, params) {
  14129. events.trigger(this, event, params);
  14130. };
  14131. /**
  14132. * Create the main frame for the Graph.
  14133. * This function is executed once when a Graph object is created. The frame
  14134. * contains a canvas, and this canvas contains all objects like the axis and
  14135. * nodes.
  14136. * @private
  14137. */
  14138. Graph.prototype._create = function () {
  14139. // remove all elements from the container element.
  14140. while (this.containerElement.hasChildNodes()) {
  14141. this.containerElement.removeChild(this.containerElement.firstChild);
  14142. }
  14143. this.frame = document.createElement('div');
  14144. this.frame.className = 'graph-frame';
  14145. this.frame.style.position = 'relative';
  14146. this.frame.style.overflow = 'hidden';
  14147. // create the graph canvas (HTML canvas element)
  14148. this.frame.canvas = document.createElement( 'canvas' );
  14149. this.frame.canvas.style.position = 'relative';
  14150. this.frame.appendChild(this.frame.canvas);
  14151. if (!this.frame.canvas.getContext) {
  14152. var noCanvas = document.createElement( 'DIV' );
  14153. noCanvas.style.color = 'red';
  14154. noCanvas.style.fontWeight = 'bold' ;
  14155. noCanvas.style.padding = '10px';
  14156. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  14157. this.frame.canvas.appendChild(noCanvas);
  14158. }
  14159. var me = this;
  14160. this.drag = {};
  14161. this.pinch = {};
  14162. this.hammer = Hammer(this.frame.canvas, {
  14163. prevent_default: true
  14164. });
  14165. this.hammer.on('tap', me._onTap.bind(me) );
  14166. this.hammer.on('hold', me._onHold.bind(me) );
  14167. this.hammer.on('pinch', me._onPinch.bind(me) );
  14168. this.hammer.on('touch', me._onTouch.bind(me) );
  14169. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  14170. this.hammer.on('drag', me._onDrag.bind(me) );
  14171. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  14172. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  14173. this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
  14174. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  14175. this.mouseTrap = mouseTrap;
  14176. this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me));
  14177. this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
  14178. this.mouseTrap.bind("s",this.singleStep.bind(me));
  14179. this.mouseTrap.bind("h",this.forceAggregateHubs.bind(me));
  14180. this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
  14181. // add the frame to the container element
  14182. this.containerElement.appendChild(this.frame);
  14183. };
  14184. /**
  14185. *
  14186. * @param {{x: Number, y: Number}} pointer
  14187. * @return {Number | null} node
  14188. * @private
  14189. */
  14190. Graph.prototype._getNodeAt = function (pointer) {
  14191. var x = this._canvasToX(pointer.x);
  14192. var y = this._canvasToY(pointer.y);
  14193. var obj = {
  14194. left: x,
  14195. top: y,
  14196. right: x,
  14197. bottom: y
  14198. };
  14199. // if there are overlapping nodes, select the last one, this is the
  14200. // one which is drawn on top of the others
  14201. var overlappingNodes = this._getNodesOverlappingWith(obj);
  14202. return (overlappingNodes.length > 0) ?
  14203. overlappingNodes[overlappingNodes.length - 1] : null;
  14204. };
  14205. /**
  14206. * Get the pointer location from a touch location
  14207. * @param {{pageX: Number, pageY: Number}} touch
  14208. * @return {{x: Number, y: Number}} pointer
  14209. * @private
  14210. */
  14211. Graph.prototype._getPointer = function (touch) {
  14212. return {
  14213. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  14214. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  14215. };
  14216. };
  14217. /**
  14218. * On start of a touch gesture, store the pointer
  14219. * @param event
  14220. * @private
  14221. */
  14222. Graph.prototype._onTouch = function (event) {
  14223. this.drag.pointer = this._getPointer(event.gesture.touches[0]);
  14224. this.drag.pinched = false;
  14225. this.pinch.scale = this._getScale();
  14226. };
  14227. /**
  14228. * handle drag start event
  14229. * @private
  14230. */
  14231. Graph.prototype._onDragStart = function () {
  14232. var drag = this.drag;
  14233. drag.selection = [];
  14234. drag.translation = this._getTranslation();
  14235. drag.nodeId = this._getNodeAt(drag.pointer);
  14236. // note: drag.pointer is set in _onTouch to get the initial touch location
  14237. var node = this.nodes[drag.nodeId];
  14238. if (node) {
  14239. // select the clicked node if not yet selected
  14240. if (!node.isSelected()) {
  14241. this._selectNodes([drag.nodeId]);
  14242. }
  14243. // create an array with the selected nodes and their original location and status
  14244. var me = this;
  14245. this.selection.forEach(function (id) {
  14246. var node = me.nodes[id];
  14247. if (node) {
  14248. var s = {
  14249. id: id,
  14250. node: node,
  14251. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  14252. x: node.x,
  14253. y: node.y,
  14254. xFixed: node.xFixed,
  14255. yFixed: node.yFixed
  14256. };
  14257. node.xFixed = true;
  14258. node.yFixed = true;
  14259. drag.selection.push(s);
  14260. }
  14261. });
  14262. }
  14263. };
  14264. /**
  14265. * handle drag event
  14266. * @private
  14267. */
  14268. Graph.prototype._onDrag = function (event) {
  14269. if (this.drag.pinched) {
  14270. return;
  14271. }
  14272. var pointer = this._getPointer(event.gesture.touches[0]);
  14273. var me = this,
  14274. drag = this.drag,
  14275. selection = drag.selection;
  14276. if (selection && selection.length) {
  14277. // calculate delta's and new location
  14278. var deltaX = pointer.x - drag.pointer.x,
  14279. deltaY = pointer.y - drag.pointer.y;
  14280. // update position of all selected nodes
  14281. selection.forEach(function (s) {
  14282. var node = s.node;
  14283. if (!s.xFixed) {
  14284. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  14285. }
  14286. if (!s.yFixed) {
  14287. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  14288. }
  14289. });
  14290. // start animation if not yet running
  14291. if (!this.moving) {
  14292. this.moving = true;
  14293. this.start();
  14294. }
  14295. }
  14296. else {
  14297. // move the graph
  14298. var diffX = pointer.x - this.drag.pointer.x;
  14299. var diffY = pointer.y - this.drag.pointer.y;
  14300. this._setTranslation(
  14301. this.drag.translation.x + diffX,
  14302. this.drag.translation.y + diffY);
  14303. this._redraw();
  14304. this.moved = true;
  14305. }
  14306. };
  14307. /**
  14308. * handle drag start event
  14309. * @private
  14310. */
  14311. Graph.prototype._onDragEnd = function () {
  14312. var selection = this.drag.selection;
  14313. if (selection) {
  14314. selection.forEach(function (s) {
  14315. // restore original xFixed and yFixed
  14316. s.node.xFixed = s.xFixed;
  14317. s.node.yFixed = s.yFixed;
  14318. });
  14319. }
  14320. };
  14321. /**
  14322. * handle tap/click event: select/unselect a node
  14323. * @private
  14324. */
  14325. Graph.prototype._onTap = function (event) {
  14326. var pointer = this._getPointer(event.gesture.touches[0]);
  14327. var nodeId = this._getNodeAt(pointer);
  14328. var node = this.nodes[nodeId];
  14329. var elapsedTime = new Date().getTime() - this.tapTimer;
  14330. this.tapTimer = new Date().getTime();
  14331. if (node) {
  14332. if (node.isSelected() && elapsedTime < 300) {
  14333. this.openCluster(node);
  14334. }
  14335. // select this node
  14336. this._selectNodes([nodeId]);
  14337. if (!this.moving) {
  14338. this._redraw();
  14339. }
  14340. }
  14341. else {
  14342. // remove selection
  14343. this._unselectNodes();
  14344. this._redraw();
  14345. }
  14346. };
  14347. /**
  14348. * handle long tap event: multi select nodes
  14349. * @private
  14350. */
  14351. Graph.prototype._onHold = function (event) {
  14352. var pointer = this._getPointer(event.gesture.touches[0]);
  14353. var nodeId = this._getNodeAt(pointer);
  14354. var node = this.nodes[nodeId];
  14355. if (node) {
  14356. if (!node.isSelected()) {
  14357. // select this node, keep previous selection
  14358. var append = true;
  14359. this._selectNodes([nodeId], append);
  14360. }
  14361. else {
  14362. this._unselectNodes([nodeId]);
  14363. }
  14364. if (!this.moving) {
  14365. this._redraw();
  14366. }
  14367. }
  14368. else {
  14369. // Do nothing
  14370. }
  14371. };
  14372. /**
  14373. * Handle pinch event
  14374. * @param event
  14375. * @private
  14376. */
  14377. Graph.prototype._onPinch = function (event) {
  14378. var pointer = this._getPointer(event.gesture.center);
  14379. this.drag.pinched = true;
  14380. if (!('scale' in this.pinch)) {
  14381. this.pinch.scale = 1;
  14382. }
  14383. // TODO: enable moving while pinching?
  14384. var scale = this.pinch.scale * event.gesture.scale;
  14385. this._zoom(scale, pointer)
  14386. };
  14387. /**
  14388. * Zoom the graph in or out
  14389. * @param {Number} scale a number around 1, and between 0.01 and 10
  14390. * @param {{x: Number, y: Number}} pointer
  14391. * @return {Number} appliedScale scale is limited within the boundaries
  14392. * @private
  14393. */
  14394. Graph.prototype._zoom = function(scale, pointer) {
  14395. var scaleOld = this._getScale();
  14396. if (scale < 0.01) {
  14397. scale = 0.01;
  14398. }
  14399. if (scale > 10) {
  14400. scale = 10;
  14401. }
  14402. // + this.frame.canvas.clientHeight / 2
  14403. var translation = this._getTranslation();
  14404. var scaleFrac = scale / scaleOld;
  14405. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  14406. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  14407. this.zoomCenter = {"x" : this._canvasToX(pointer.x),
  14408. "y" : this._canvasToY(pointer.y)};
  14409. // this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
  14410. this._setScale(scale);
  14411. this._setTranslation(tx, ty);
  14412. this.updateClusters(0,false,false);
  14413. this._redraw();
  14414. //console.log("current zoomscale:",this.scale)
  14415. return scale;
  14416. };
  14417. /**
  14418. * Event handler for mouse wheel event, used to zoom the timeline
  14419. * See http://adomas.org/javascript-mouse-wheel/
  14420. * https://github.com/EightMedia/hammer.js/issues/256
  14421. * @param {MouseEvent} event
  14422. * @private
  14423. */
  14424. Graph.prototype._onMouseWheel = function(event) {
  14425. // retrieve delta
  14426. var delta = 0;
  14427. if (event.wheelDelta) { /* IE/Opera. */
  14428. delta = event.wheelDelta/120;
  14429. } else if (event.detail) { /* Mozilla case. */
  14430. // In Mozilla, sign of delta is different than in IE.
  14431. // Also, delta is multiple of 3.
  14432. delta = -event.detail/3;
  14433. }
  14434. // If delta is nonzero, handle it.
  14435. // Basically, delta is now positive if wheel was scrolled up,
  14436. // and negative, if wheel was scrolled down.
  14437. if (delta) {
  14438. if (!('mousewheelScale' in this.pinch)) {
  14439. this.pinch.mousewheelScale = 1;
  14440. }
  14441. // calculate the new scale
  14442. var scale = this.pinch.mousewheelScale;
  14443. var zoom = delta / 10;
  14444. if (delta < 0) {
  14445. zoom = zoom / (1 - zoom);
  14446. }
  14447. scale *= (1 + zoom);
  14448. // calculate the pointer location
  14449. var gesture = util.fakeGesture(this, event);
  14450. var pointer = this._getPointer(gesture.center);
  14451. // apply the new scale
  14452. scale = this._zoom(scale, pointer);
  14453. // store the new, applied scale
  14454. this.pinch.mousewheelScale = scale;
  14455. }
  14456. // Prevent default actions caused by mouse wheel.
  14457. event.preventDefault();
  14458. };
  14459. /**
  14460. * Mouse move handler for checking whether the title moves over a node with a title.
  14461. * @param {Event} event
  14462. * @private
  14463. */
  14464. Graph.prototype._onMouseMoveTitle = function (event) {
  14465. var gesture = util.fakeGesture(this, event);
  14466. var pointer = this._getPointer(gesture.center);
  14467. // check if the previously selected node is still selected
  14468. if (this.popupNode) {
  14469. this._checkHidePopup(pointer);
  14470. }
  14471. // start a timeout that will check if the mouse is positioned above
  14472. // an element
  14473. var me = this;
  14474. var checkShow = function() {
  14475. me._checkShowPopup(pointer);
  14476. };
  14477. if (this.popupTimer) {
  14478. clearInterval(this.popupTimer); // stop any running timer
  14479. }
  14480. if (!this.leftButtonDown) {
  14481. this.popupTimer = setTimeout(checkShow, 300);
  14482. }
  14483. };
  14484. /**
  14485. * Check if there is an element on the given position in the graph
  14486. * (a node or edge). If so, and if this element has a title,
  14487. * show a popup window with its title.
  14488. *
  14489. * @param {{x:Number, y:Number}} pointer
  14490. * @private
  14491. */
  14492. Graph.prototype._checkShowPopup = function (pointer) {
  14493. var obj = {
  14494. left: this._canvasToX(pointer.x),
  14495. top: this._canvasToY(pointer.y),
  14496. right: this._canvasToX(pointer.x),
  14497. bottom: this._canvasToY(pointer.y)
  14498. };
  14499. var id;
  14500. var lastPopupNode = this.popupNode;
  14501. if (this.popupNode == undefined) {
  14502. // search the nodes for overlap, select the top one in case of multiple nodes
  14503. var nodes = this.nodes;
  14504. for (id in nodes) {
  14505. if (nodes.hasOwnProperty(id)) {
  14506. var node = nodes[id];
  14507. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  14508. this.popupNode = node;
  14509. break;
  14510. }
  14511. }
  14512. }
  14513. }
  14514. if (this.popupNode == undefined) {
  14515. // search the edges for overlap
  14516. var edges = this.edges;
  14517. for (id in edges) {
  14518. if (edges.hasOwnProperty(id)) {
  14519. var edge = edges[id];
  14520. if (edge.connected && (edge.getTitle() != undefined) &&
  14521. edge.isOverlappingWith(obj)) {
  14522. this.popupNode = edge;
  14523. break;
  14524. }
  14525. }
  14526. }
  14527. }
  14528. if (this.popupNode) {
  14529. // show popup message window
  14530. if (this.popupNode != lastPopupNode) {
  14531. var me = this;
  14532. if (!me.popup) {
  14533. me.popup = new Popup(me.frame);
  14534. }
  14535. // adjust a small offset such that the mouse cursor is located in the
  14536. // bottom left location of the popup, and you can easily move over the
  14537. // popup area
  14538. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  14539. me.popup.setText(me.popupNode.getTitle());
  14540. me.popup.show();
  14541. }
  14542. }
  14543. else {
  14544. if (this.popup) {
  14545. this.popup.hide();
  14546. }
  14547. }
  14548. };
  14549. /**
  14550. * Check if the popup must be hided, which is the case when the mouse is no
  14551. * longer hovering on the object
  14552. * @param {{x:Number, y:Number}} pointer
  14553. * @private
  14554. */
  14555. Graph.prototype._checkHidePopup = function (pointer) {
  14556. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  14557. this.popupNode = undefined;
  14558. if (this.popup) {
  14559. this.popup.hide();
  14560. }
  14561. }
  14562. };
  14563. /**
  14564. * Unselect selected nodes. If no selection array is provided, all nodes
  14565. * are unselected
  14566. * @param {Object[]} selection Array with selection objects, each selection
  14567. * object has a parameter row. Optional
  14568. * @param {Boolean} triggerSelect If true (default), the select event
  14569. * is triggered when nodes are unselected
  14570. * @return {Boolean} changed True if the selection is changed
  14571. * @private
  14572. */
  14573. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  14574. var changed = false;
  14575. var i, iMax, id;
  14576. if (selection) {
  14577. // remove provided selections
  14578. for (i = 0, iMax = selection.length; i < iMax; i++) {
  14579. id = selection[i];
  14580. this.nodes[id].unselect();
  14581. var j = 0;
  14582. while (j < this.selection.length) {
  14583. if (this.selection[j] == id) {
  14584. this.selection.splice(j, 1);
  14585. changed = true;
  14586. }
  14587. else {
  14588. j++;
  14589. }
  14590. }
  14591. }
  14592. }
  14593. else if (this.selection && this.selection.length) {
  14594. // remove all selections
  14595. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  14596. id = this.selection[i];
  14597. this.nodes[id].unselect();
  14598. changed = true;
  14599. }
  14600. this.selection = [];
  14601. }
  14602. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  14603. // fire the select event
  14604. this._trigger('select');
  14605. }
  14606. return changed;
  14607. };
  14608. /**
  14609. * select all nodes on given location x, y
  14610. * @param {Array} selection an array with node ids
  14611. * @param {boolean} append If true, the new selection will be appended to the
  14612. * current selection (except for duplicate entries)
  14613. * @return {Boolean} changed True if the selection is changed
  14614. * @private
  14615. */
  14616. Graph.prototype._selectNodes = function(selection, append) {
  14617. var changed = false;
  14618. var i, iMax;
  14619. // TODO: the selectNodes method is a little messy, rework this
  14620. // check if the current selection equals the desired selection
  14621. var selectionAlreadyThere = true;
  14622. if (selection.length != this.selection.length) {
  14623. selectionAlreadyThere = false;
  14624. }
  14625. else {
  14626. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  14627. if (selection[i] != this.selection[i]) {
  14628. selectionAlreadyThere = false;
  14629. break;
  14630. }
  14631. }
  14632. }
  14633. if (selectionAlreadyThere) {
  14634. return changed;
  14635. }
  14636. if (append == undefined || append == false) {
  14637. // first deselect any selected node
  14638. var triggerSelect = false;
  14639. changed = this._unselectNodes(undefined, triggerSelect);
  14640. }
  14641. for (i = 0, iMax = selection.length; i < iMax; i++) {
  14642. // add each of the new selections, but only when they are not duplicate
  14643. var id = selection[i];
  14644. var isDuplicate = (this.selection.indexOf(id) != -1);
  14645. if (!isDuplicate) {
  14646. this.nodes[id].select();
  14647. this.selection.push(id);
  14648. changed = true;
  14649. }
  14650. }
  14651. if (changed) {
  14652. // fire the select event
  14653. this._trigger('select');
  14654. }
  14655. return changed;
  14656. };
  14657. /**
  14658. * retrieve all nodes overlapping with given object
  14659. * @param {Object} obj An object with parameters left, top, right, bottom
  14660. * @return {Number[]} An array with id's of the overlapping nodes
  14661. * @private
  14662. */
  14663. Graph.prototype._getNodesOverlappingWith = function (obj) {
  14664. var nodes = this.nodes,
  14665. overlappingNodes = [];
  14666. for (var id in nodes) {
  14667. if (nodes.hasOwnProperty(id)) {
  14668. if (nodes[id].isOverlappingWith(obj)) {
  14669. overlappingNodes.push(id);
  14670. }
  14671. }
  14672. }
  14673. return overlappingNodes;
  14674. };
  14675. /**
  14676. * retrieve the currently selected nodes
  14677. * @return {Number[] | String[]} selection An array with the ids of the
  14678. * selected nodes.
  14679. */
  14680. Graph.prototype.getSelection = function() {
  14681. return this.selection.concat([]);
  14682. };
  14683. /**
  14684. * select zero or more nodes
  14685. * @param {Number[] | String[]} selection An array with the ids of the
  14686. * selected nodes.
  14687. */
  14688. Graph.prototype.setSelection = function(selection) {
  14689. var i, iMax, id;
  14690. if (!selection || (selection.length == undefined))
  14691. throw 'Selection must be an array with ids';
  14692. // first unselect any selected node
  14693. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  14694. id = this.selection[i];
  14695. this.nodes[id].unselect();
  14696. }
  14697. this.selection = [];
  14698. for (i = 0, iMax = selection.length; i < iMax; i++) {
  14699. id = selection[i];
  14700. var node = this.nodes[id];
  14701. if (!node) {
  14702. throw new RangeError('Node with id "' + id + '" not found');
  14703. }
  14704. node.select();
  14705. this.selection.push(id);
  14706. }
  14707. this.redraw();
  14708. };
  14709. /**
  14710. * Validate the selection: remove ids of nodes which no longer exist
  14711. * @private
  14712. */
  14713. Graph.prototype._updateSelection = function () {
  14714. var i = 0;
  14715. while (i < this.selection.length) {
  14716. var id = this.selection[i];
  14717. if (!this.nodes[id]) {
  14718. this.selection.splice(i, 1);
  14719. }
  14720. else {
  14721. i++;
  14722. }
  14723. }
  14724. };
  14725. /**
  14726. * Temporary method to test calculating a hub value for the nodes
  14727. * @param {number} level Maximum number edges between two nodes in order
  14728. * to call them connected. Optional, 1 by default
  14729. * @return {Number[]} connectioncount array with the connection count
  14730. * for each node
  14731. * @private
  14732. */
  14733. Graph.prototype._getConnectionCount = function(level) {
  14734. if (level == undefined) {
  14735. level = 1;
  14736. }
  14737. // get the nodes connected to given nodes
  14738. function getConnectedNodes(nodes) {
  14739. var connectedNodes = [];
  14740. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  14741. var node = nodes[j];
  14742. // find all nodes connected to this node
  14743. var edges = node.edges;
  14744. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  14745. var edge = edges[i];
  14746. var other = null;
  14747. // check if connected
  14748. if (edge.from == node)
  14749. other = edge.to;
  14750. else if (edge.to == node)
  14751. other = edge.from;
  14752. // check if the other node is not already in the list with nodes
  14753. var k, kMax;
  14754. if (other) {
  14755. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  14756. if (nodes[k] == other) {
  14757. other = null;
  14758. break;
  14759. }
  14760. }
  14761. }
  14762. if (other) {
  14763. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  14764. if (connectedNodes[k] == other) {
  14765. other = null;
  14766. break;
  14767. }
  14768. }
  14769. }
  14770. if (other)
  14771. connectedNodes.push(other);
  14772. }
  14773. }
  14774. return connectedNodes;
  14775. }
  14776. var connections = [];
  14777. var nodes = this.nodes;
  14778. for (var id in nodes) {
  14779. if (nodes.hasOwnProperty(id)) {
  14780. var c = [nodes[id]];
  14781. for (var l = 0; l < level; l++) {
  14782. c = c.concat(getConnectedNodes(c));
  14783. }
  14784. connections.push(c);
  14785. }
  14786. }
  14787. var hubs = [];
  14788. for (var i = 0, len = connections.length; i < len; i++) {
  14789. hubs.push(connections[i].length);
  14790. }
  14791. return hubs;
  14792. };
  14793. /**
  14794. * Set a new size for the graph
  14795. * @param {string} width Width in pixels or percentage (for example '800px'
  14796. * or '50%')
  14797. * @param {string} height Height in pixels or percentage (for example '400px'
  14798. * or '30%')
  14799. */
  14800. Graph.prototype.setSize = function(width, height) {
  14801. this.frame.style.width = width;
  14802. this.frame.style.height = height;
  14803. this.frame.canvas.style.width = '100%';
  14804. this.frame.canvas.style.height = '100%';
  14805. this.frame.canvas.width = this.frame.canvas.clientWidth;
  14806. this.frame.canvas.height = this.frame.canvas.clientHeight;
  14807. };
  14808. /**
  14809. * Set a data set with nodes for the graph
  14810. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  14811. * @private
  14812. */
  14813. Graph.prototype._setNodes = function(nodes) {
  14814. var oldNodesData = this.nodesData;
  14815. if (nodes instanceof DataSet || nodes instanceof DataView) {
  14816. this.nodesData = nodes;
  14817. }
  14818. else if (nodes instanceof Array) {
  14819. this.nodesData = new DataSet();
  14820. this.nodesData.add(nodes);
  14821. }
  14822. else if (!nodes) {
  14823. this.nodesData = new DataSet();
  14824. }
  14825. else {
  14826. throw new TypeError('Array or DataSet expected');
  14827. }
  14828. if (oldNodesData) {
  14829. // unsubscribe from old dataset
  14830. util.forEach(this.nodesListeners, function (callback, event) {
  14831. oldNodesData.unsubscribe(event, callback);
  14832. });
  14833. }
  14834. // remove drawn nodes
  14835. this.nodes = {};
  14836. if (this.nodesData) {
  14837. // subscribe to new dataset
  14838. var me = this;
  14839. util.forEach(this.nodesListeners, function (callback, event) {
  14840. me.nodesData.subscribe(event, callback);
  14841. });
  14842. // draw all new nodes
  14843. var ids = this.nodesData.getIds();
  14844. this._addNodes(ids);
  14845. }
  14846. this._updateSelection();
  14847. };
  14848. /**
  14849. * Add nodes
  14850. * @param {Number[] | String[]} ids
  14851. * @private
  14852. */
  14853. Graph.prototype._addNodes = function(ids) {
  14854. var id;
  14855. for (var i = 0, len = ids.length; i < len; i++) {
  14856. id = ids[i];
  14857. var data = this.nodesData.get(id);
  14858. var node = new Node(data, this.images, this.groups, this.constants);
  14859. this.nodes[id] = node; // note: this may replace an existing node
  14860. if (!node.isFixed()) {
  14861. // TODO: position new nodes in a smarter way!
  14862. var radius = this.constants.edges.length * 2;
  14863. var count = ids.length;
  14864. var angle = 2 * Math.PI * (i / count);
  14865. node.x = radius * Math.cos(angle);
  14866. node.y = radius * Math.sin(angle);
  14867. // note: no not use node.isMoving() here, as that gives the current
  14868. // velocity of the node, which is zero after creation of the node.
  14869. this.moving = true;
  14870. }
  14871. }
  14872. this._updateNodeIndexList();
  14873. this._reconnectEdges();
  14874. this._updateValueRange(this.nodes);
  14875. };
  14876. /**
  14877. * Update existing nodes, or create them when not yet existing
  14878. * @param {Number[] | String[]} ids
  14879. * @private
  14880. */
  14881. Graph.prototype._updateNodes = function(ids) {
  14882. var nodes = this.nodes,
  14883. nodesData = this.nodesData;
  14884. for (var i = 0, len = ids.length; i < len; i++) {
  14885. var id = ids[i];
  14886. var node = nodes[id];
  14887. var data = nodesData.get(id);
  14888. if (node) {
  14889. // update node
  14890. node.setProperties(data, this.constants);
  14891. }
  14892. else {
  14893. // create node
  14894. node = new Node(properties, this.images, this.groups, this.constants);
  14895. nodes[id] = node;
  14896. if (!node.isFixed()) {
  14897. this.moving = true;
  14898. }
  14899. }
  14900. }
  14901. this._updateNodeIndexList();
  14902. this._reconnectEdges();
  14903. this._updateValueRange(nodes);
  14904. };
  14905. /**
  14906. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  14907. * @param {Number[] | String[]} ids
  14908. * @private
  14909. */
  14910. Graph.prototype._removeNodes = function(ids) {
  14911. var nodes = this.nodes;
  14912. for (var i = 0, len = ids.length; i < len; i++) {
  14913. var id = ids[i];
  14914. delete nodes[id];
  14915. }
  14916. this._updateNodeIndexList();
  14917. this._reconnectEdges();
  14918. this._updateSelection();
  14919. this._updateValueRange(nodes);
  14920. };
  14921. /**
  14922. * Load edges by reading the data table
  14923. * @param {Array | DataSet | DataView} edges The data containing the edges.
  14924. * @private
  14925. * @private
  14926. */
  14927. Graph.prototype._setEdges = function(edges) {
  14928. var oldEdgesData = this.edgesData;
  14929. if (edges instanceof DataSet || edges instanceof DataView) {
  14930. this.edgesData = edges;
  14931. }
  14932. else if (edges instanceof Array) {
  14933. this.edgesData = new DataSet();
  14934. this.edgesData.add(edges);
  14935. }
  14936. else if (!edges) {
  14937. this.edgesData = new DataSet();
  14938. }
  14939. else {
  14940. throw new TypeError('Array or DataSet expected');
  14941. }
  14942. if (oldEdgesData) {
  14943. // unsubscribe from old dataset
  14944. util.forEach(this.edgesListeners, function (callback, event) {
  14945. oldEdgesData.unsubscribe(event, callback);
  14946. });
  14947. }
  14948. // remove drawn edges
  14949. this.edges = {};
  14950. if (this.edgesData) {
  14951. // subscribe to new dataset
  14952. var me = this;
  14953. util.forEach(this.edgesListeners, function (callback, event) {
  14954. me.edgesData.subscribe(event, callback);
  14955. });
  14956. // draw all new nodes
  14957. var ids = this.edgesData.getIds();
  14958. this._addEdges(ids);
  14959. }
  14960. this._reconnectEdges();
  14961. };
  14962. /**
  14963. * Add edges
  14964. * @param {Number[] | String[]} ids
  14965. * @private
  14966. */
  14967. Graph.prototype._addEdges = function (ids) {
  14968. var edges = this.edges,
  14969. edgesData = this.edgesData;
  14970. for (var i = 0, len = ids.length; i < len; i++) {
  14971. var id = ids[i];
  14972. var oldEdge = edges[id];
  14973. if (oldEdge) {
  14974. oldEdge.disconnect();
  14975. }
  14976. var data = edgesData.get(id, {"showInternalIds" : true});
  14977. edges[id] = new Edge(data, this, this.constants);
  14978. }
  14979. this.moving = true;
  14980. this._updateValueRange(edges);
  14981. };
  14982. /**
  14983. * Update existing edges, or create them when not yet existing
  14984. * @param {Number[] | String[]} ids
  14985. * @private
  14986. */
  14987. Graph.prototype._updateEdges = function (ids) {
  14988. var edges = this.edges,
  14989. edgesData = this.edgesData;
  14990. for (var i = 0, len = ids.length; i < len; i++) {
  14991. var id = ids[i];
  14992. var data = edgesData.get(id);
  14993. var edge = edges[id];
  14994. if (edge) {
  14995. // update edge
  14996. edge.disconnect();
  14997. edge.setProperties(data, this.constants);
  14998. edge.connect();
  14999. }
  15000. else {
  15001. // create edge
  15002. edge = new Edge(data, this, this.constants);
  15003. this.edges[id] = edge;
  15004. }
  15005. }
  15006. this.moving = true;
  15007. this._updateValueRange(edges);
  15008. };
  15009. /**
  15010. * Remove existing edges. Non existing ids will be ignored
  15011. * @param {Number[] | String[]} ids
  15012. * @private
  15013. */
  15014. Graph.prototype._removeEdges = function (ids) {
  15015. var edges = this.edges;
  15016. for (var i = 0, len = ids.length; i < len; i++) {
  15017. var id = ids[i];
  15018. var edge = edges[id];
  15019. if (edge) {
  15020. edge.disconnect();
  15021. delete edges[id];
  15022. }
  15023. }
  15024. this.moving = true;
  15025. this._updateValueRange(edges);
  15026. };
  15027. /**
  15028. * Reconnect all edges
  15029. * @private
  15030. */
  15031. Graph.prototype._reconnectEdges = function() {
  15032. var id,
  15033. nodes = this.nodes,
  15034. edges = this.edges;
  15035. for (id in nodes) {
  15036. if (nodes.hasOwnProperty(id)) {
  15037. nodes[id].edges = [];
  15038. }
  15039. }
  15040. for (id in edges) {
  15041. if (edges.hasOwnProperty(id)) {
  15042. var edge = edges[id];
  15043. edge.from = null;
  15044. edge.to = null;
  15045. edge.connect();
  15046. }
  15047. }
  15048. };
  15049. /**
  15050. * Update the values of all object in the given array according to the current
  15051. * value range of the objects in the array.
  15052. * @param {Object} obj An object containing a set of Edges or Nodes
  15053. * The objects must have a method getValue() and
  15054. * setValueRange(min, max).
  15055. * @private
  15056. */
  15057. Graph.prototype._updateValueRange = function(obj) {
  15058. var id;
  15059. // determine the range of the objects
  15060. var valueMin = undefined;
  15061. var valueMax = undefined;
  15062. for (id in obj) {
  15063. if (obj.hasOwnProperty(id)) {
  15064. var value = obj[id].getValue();
  15065. if (value !== undefined) {
  15066. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  15067. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  15068. }
  15069. }
  15070. }
  15071. // adjust the range of all objects
  15072. if (valueMin !== undefined && valueMax !== undefined) {
  15073. for (id in obj) {
  15074. if (obj.hasOwnProperty(id)) {
  15075. obj[id].setValueRange(valueMin, valueMax);
  15076. }
  15077. }
  15078. }
  15079. };
  15080. /**
  15081. * Redraw the graph with the current data
  15082. * chart will be resized too.
  15083. */
  15084. Graph.prototype.redraw = function() {
  15085. this.setSize(this.width, this.height);
  15086. this._redraw();
  15087. };
  15088. /**
  15089. * Redraw the graph with the current data
  15090. * @private
  15091. */
  15092. Graph.prototype._redraw = function() {
  15093. var ctx = this.frame.canvas.getContext('2d');
  15094. // clear the canvas
  15095. var w = this.frame.canvas.width;
  15096. var h = this.frame.canvas.height;
  15097. ctx.clearRect(0, 0, w, h);
  15098. // set scaling and translation
  15099. ctx.save();
  15100. ctx.translate(this.translation.x, this.translation.y);
  15101. ctx.scale(this.scale, this.scale);
  15102. this._drawEdges(ctx);
  15103. this._drawNodes(ctx);
  15104. // restore original scaling and translation
  15105. ctx.restore();
  15106. };
  15107. /**
  15108. * Set the translation of the graph
  15109. * @param {Number} offsetX Horizontal offset
  15110. * @param {Number} offsetY Vertical offset
  15111. * @private
  15112. */
  15113. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  15114. if (this.translation === undefined) {
  15115. this.translation = {
  15116. x: 0,
  15117. y: 0
  15118. };
  15119. }
  15120. if (offsetX !== undefined) {
  15121. this.translation.x = offsetX;
  15122. }
  15123. if (offsetY !== undefined) {
  15124. this.translation.y = offsetY;
  15125. }
  15126. };
  15127. /**
  15128. * Get the translation of the graph
  15129. * @return {Object} translation An object with parameters x and y, both a number
  15130. * @private
  15131. */
  15132. Graph.prototype._getTranslation = function() {
  15133. return {
  15134. x: this.translation.x,
  15135. y: this.translation.y
  15136. };
  15137. };
  15138. /**
  15139. * Scale the graph
  15140. * @param {Number} scale Scaling factor 1.0 is unscaled
  15141. * @private
  15142. */
  15143. Graph.prototype._setScale = function(scale) {
  15144. this.scale = scale;
  15145. };
  15146. /**
  15147. * Get the current scale of the graph
  15148. * @return {Number} scale Scaling factor 1.0 is unscaled
  15149. * @private
  15150. */
  15151. Graph.prototype._getScale = function() {
  15152. return this.scale;
  15153. };
  15154. /**
  15155. * Convert a horizontal point on the HTML canvas to the x-value of the model
  15156. * @param {number} x
  15157. * @returns {number}
  15158. * @private
  15159. */
  15160. Graph.prototype._canvasToX = function(x) {
  15161. return (x - this.translation.x) / this.scale;
  15162. };
  15163. /**
  15164. * Convert an x-value in the model to a horizontal point on the HTML canvas
  15165. * @param {number} x
  15166. * @returns {number}
  15167. * @private
  15168. */
  15169. Graph.prototype._xToCanvas = function(x) {
  15170. return x * this.scale + this.translation.x;
  15171. };
  15172. /**
  15173. * Convert a vertical point on the HTML canvas to the y-value of the model
  15174. * @param {number} y
  15175. * @returns {number}
  15176. * @private
  15177. */
  15178. Graph.prototype._canvasToY = function(y) {
  15179. return (y - this.translation.y) / this.scale;
  15180. };
  15181. /**
  15182. * Convert an y-value in the model to a vertical point on the HTML canvas
  15183. * @param {number} y
  15184. * @returns {number}
  15185. * @private
  15186. */
  15187. Graph.prototype._yToCanvas = function(y) {
  15188. return y * this.scale + this.translation.y ;
  15189. };
  15190. /**
  15191. * Redraw all nodes
  15192. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15193. * @param {CanvasRenderingContext2D} ctx
  15194. * @private
  15195. */
  15196. Graph.prototype._drawNodes = function(ctx) {
  15197. // first draw the unselected nodes
  15198. var nodes = this.nodes;
  15199. var selected = [];
  15200. for (var id in nodes) {
  15201. if (nodes.hasOwnProperty(id)) {
  15202. nodes[id].setScale(this.scale);
  15203. if (nodes[id].isSelected()) {
  15204. selected.push(id);
  15205. }
  15206. else {
  15207. nodes[id].draw(ctx);
  15208. }
  15209. }
  15210. }
  15211. // draw the selected nodes on top
  15212. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  15213. nodes[selected[s]].draw(ctx);
  15214. }
  15215. };
  15216. /**
  15217. * Redraw all edges
  15218. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  15219. * @param {CanvasRenderingContext2D} ctx
  15220. * @private
  15221. */
  15222. Graph.prototype._drawEdges = function(ctx) {
  15223. var edges = this.edges;
  15224. for (var id in edges) {
  15225. if (edges.hasOwnProperty(id)) {
  15226. var edge = edges[id];
  15227. edge.setScale(this.scale);
  15228. if (edge.connected) {
  15229. edges[id].draw(ctx);
  15230. }
  15231. }
  15232. }
  15233. };
  15234. /**
  15235. * Find a stable position for all nodes
  15236. * @private
  15237. */
  15238. Graph.prototype._doStabilize = function() {
  15239. var start = new Date();
  15240. // find stable position
  15241. var count = 0;
  15242. var vmin = this.constants.minVelocity;
  15243. var stable = false;
  15244. while (!stable && count < this.constants.maxIterations) {
  15245. this._calculateForces();
  15246. this._discreteStepNodes();
  15247. stable = !this._isMoving(vmin);
  15248. count++;
  15249. }
  15250. var end = new Date();
  15251. // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
  15252. };
  15253. /**
  15254. * Calculate the external forces acting on the nodes
  15255. * Forces are caused by: edges, repulsing forces between nodes, gravity
  15256. * @private
  15257. */
  15258. Graph.prototype._calculateForces = function() {
  15259. if (this.nodeIndices.length == 1) { // stop calculation if there is only one node
  15260. this.nodes[this.nodeIndices[0]]._setForce(0,0);
  15261. }
  15262. else if (this.nodeIndices.length > this.constants.clustering.maxNumberOfNodes * 4) {
  15263. console.log(this.nodeIndices.length, this.constants.clustering.maxNumberOfNodes * 4)
  15264. this.clusterToFit(false);
  15265. this._calculateForces();
  15266. }
  15267. else {
  15268. // create a local edge to the nodes and edges, that is faster
  15269. var id, dx, dy, angle, distance, fx, fy,
  15270. repulsingForce, springForce, length, edgeLength,
  15271. nodes = this.nodes,
  15272. edges = this.edges;
  15273. // Gravity is required to keep separated groups from floating off
  15274. // the forces are reset to zero in this loop by using _setForce instead
  15275. // of _addForce
  15276. var gravity = 0.08;
  15277. for (var i = 0; i < this.nodeIndices.length; i++) {
  15278. var node = nodes[this.nodeIndices[i]];
  15279. dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5;
  15280. dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*0.5;
  15281. angle = Math.atan2(dy, dx);
  15282. fx = Math.cos(angle) * gravity;
  15283. fy = Math.sin(angle) * gravity;
  15284. node._setForce(fx, fy);
  15285. node.updateDamping(this.nodeIndices.length);
  15286. }
  15287. this._updateLabels();
  15288. // repulsing forces between nodes
  15289. var minimumDistance = this.constants.nodes.distance,
  15290. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  15291. // we loop from i over all but the last entree in the array
  15292. // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j
  15293. for (var i = 0; i < this.nodeIndices.length-1; i++) {
  15294. var node1 = nodes[this.nodeIndices[i]];
  15295. for (var j = i+1; j < this.nodeIndices.length; j++) {
  15296. var node2 = nodes[this.nodeIndices[j]];
  15297. var clusterSize = (node1.clusterSize + node2.clusterSize - 2);
  15298. dx = node2.x - node1.x;
  15299. dy = node2.y - node1.y;
  15300. distance = Math.sqrt(dx * dx + dy * dy);
  15301. // clusters have a larger region of influence
  15302. minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification));
  15303. if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
  15304. angle = Math.atan2(dy, dx);
  15305. if (distance < 0.5*minimumDistance) { // at 0.5 * the minimum distance, the force is 0.993307
  15306. repulsingForce = 1.0;
  15307. }
  15308. else {
  15309. // TODO: correct factor for repulsing force
  15310. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  15311. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  15312. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  15313. }
  15314. // amplify the repulsion for clusters.
  15315. repulsingForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.forceAmplification;
  15316. fx = Math.cos(angle) * repulsingForce;
  15317. fy = Math.sin(angle) * repulsingForce;
  15318. node1._addForce(-fx, -fy);
  15319. node2._addForce(fx, fy);
  15320. }
  15321. }
  15322. }
  15323. // TODO: re-implement repulsion of edges
  15324. for (var n = 0; n < nodes.length; n++) {
  15325. for (var l = 0; l < edges.length; l++) {
  15326. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  15327. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  15328. // calculate normally distributed force
  15329. dx = nodes[n].x - lx,
  15330. dy = nodes[n].y - ly,
  15331. distance = Math.sqrt(dx * dx + dy * dy),
  15332. angle = Math.atan2(dy, dx),
  15333. // TODO: correct factor for repulsing force
  15334. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  15335. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  15336. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  15337. fx = Math.cos(angle) * repulsingforce,
  15338. fy = Math.sin(angle) * repulsingforce;
  15339. nodes[n]._addForce(fx, fy);
  15340. edges[l].from._addForce(-fx/2,-fy/2);
  15341. edges[l].to._addForce(-fx/2,-fy/2);
  15342. }
  15343. }
  15344. // forces caused by the edges, modelled as springs
  15345. for (id in edges) {
  15346. if (edges.hasOwnProperty(id)) {
  15347. var edge = edges[id];
  15348. if (edge.connected) {
  15349. var clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
  15350. dx = (edge.to.x - edge.from.x);
  15351. dy = (edge.to.y - edge.from.y);
  15352. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  15353. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  15354. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  15355. edgeLength = edge.length;
  15356. // this implies that the edges between big clusters are longer
  15357. edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
  15358. length = Math.sqrt(dx * dx + dy * dy);
  15359. angle = Math.atan2(dy, dx);
  15360. springForce = edge.stiffness * (edgeLength - length);
  15361. // boost strength of cluster springs
  15362. springForce *= (clusterSize == 0) ? 1 : 1 + clusterSize * this.constants.clustering.edgeStrength;
  15363. fx = Math.cos(angle) * springForce;
  15364. fy = Math.sin(angle) * springForce;
  15365. edge.from._addForce(-fx, -fy);
  15366. edge.to._addForce(fx, fy);
  15367. }
  15368. }
  15369. }
  15370. /*
  15371. // TODO: re-implement repulsion of edges
  15372. // repulsing forces between edges
  15373. var minimumDistance = this.constants.edges.distance,
  15374. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  15375. for (var l = 0; l < edges.length; l++) {
  15376. //Keep distance from other edge centers
  15377. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  15378. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  15379. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  15380. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  15381. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  15382. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  15383. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  15384. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  15385. // calculate normally distributed force
  15386. dx = l2x - lx,
  15387. dy = l2y - ly,
  15388. distance = Math.sqrt(dx * dx + dy * dy),
  15389. angle = Math.atan2(dy, dx),
  15390. // TODO: correct factor for repulsing force
  15391. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  15392. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  15393. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  15394. fx = Math.cos(angle) * repulsingforce,
  15395. fy = Math.sin(angle) * repulsingforce;
  15396. edges[l].from._addForce(-fx, -fy);
  15397. edges[l].to._addForce(-fx, -fy);
  15398. edges[l2].from._addForce(fx, fy);
  15399. edges[l2].to._addForce(fx, fy);
  15400. }
  15401. }
  15402. */
  15403. }
  15404. };
  15405. /**
  15406. * Check if any of the nodes is still moving
  15407. * @param {number} vmin the minimum velocity considered as 'moving'
  15408. * @return {boolean} true if moving, false if non of the nodes is moving
  15409. * @private
  15410. */
  15411. Graph.prototype._isMoving = function(vmin) {
  15412. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  15413. var nodes = this.nodes;
  15414. for (var id in nodes) {
  15415. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  15416. return true;
  15417. }
  15418. }
  15419. return false;
  15420. };
  15421. /**
  15422. * Perform one discrete step for all nodes
  15423. * @private
  15424. */
  15425. Graph.prototype._discreteStepNodes = function() {
  15426. var interval = this.refreshRate / 1000.0; // in seconds
  15427. var nodes = this.nodes;
  15428. for (var id in nodes) {
  15429. if (nodes.hasOwnProperty(id)) {
  15430. nodes[id].discreteStep(interval);
  15431. }
  15432. }
  15433. };
  15434. /**
  15435. * Start animating nodes and edges
  15436. */
  15437. Graph.prototype.start = function() {
  15438. if (!this.freezeSimulation) {
  15439. if (this.moving) {
  15440. this._calculateForces();
  15441. this._discreteStepNodes();
  15442. var vmin = this.constants.minVelocity;
  15443. this.moving = this._isMoving(vmin);
  15444. }
  15445. if (this.moving) {
  15446. // start animation. only start timer if it is not already running
  15447. if (!this.timer) {
  15448. var graph = this;
  15449. this.timer = window.setTimeout(function () {
  15450. graph.timer = undefined;
  15451. // benchmark the calculation
  15452. // var start = window.performance.now();
  15453. graph.start();
  15454. // Optionally call this twice for faster convergence
  15455. graph.start();
  15456. // var end = window.performance.now();
  15457. // var time = end - start;
  15458. // console.log('Simulation time: ' + time);
  15459. // start = window.performance.now();
  15460. graph._redraw();
  15461. // end = window.performance.now();
  15462. // time = end - start;
  15463. // console.log('Drawing time: ' + time);
  15464. }, this.refreshRate);
  15465. }
  15466. }
  15467. else {
  15468. this._redraw();
  15469. }
  15470. }
  15471. };
  15472. Graph.prototype.singleStep = function() {
  15473. if (this.moving) {
  15474. this._calculateForces();
  15475. this._discreteStepNodes();
  15476. var vmin = this.constants.minVelocity;
  15477. this.moving = this._isMoving(vmin);
  15478. this._redraw();
  15479. }
  15480. };
  15481. /**
  15482. * Stop animating nodes and edges.
  15483. */
  15484. Graph.prototype.stop = function () {
  15485. if (this.timer) {
  15486. window.clearInterval(this.timer);
  15487. this.timer = undefined;
  15488. }
  15489. };
  15490. /**
  15491. * Freeze the animation
  15492. */
  15493. Graph.prototype.toggleFreeze = function() {
  15494. if (this.freezeSimulation == false) {
  15495. this.freezeSimulation = true;
  15496. }
  15497. else {
  15498. this.freezeSimulation = false;
  15499. this.start();
  15500. }
  15501. console.log('freezeSimulation',this.freezeSimulation)
  15502. }
  15503. /**
  15504. * vis.js module exports
  15505. */
  15506. var vis = {
  15507. util: util,
  15508. events: events,
  15509. Controller: Controller,
  15510. DataSet: DataSet,
  15511. DataView: DataView,
  15512. Range: Range,
  15513. Stack: Stack,
  15514. TimeStep: TimeStep,
  15515. EventBus: EventBus,
  15516. components: {
  15517. items: {
  15518. Item: Item,
  15519. ItemBox: ItemBox,
  15520. ItemPoint: ItemPoint,
  15521. ItemRange: ItemRange
  15522. },
  15523. Component: Component,
  15524. Panel: Panel,
  15525. RootPanel: RootPanel,
  15526. ItemSet: ItemSet,
  15527. TimeAxis: TimeAxis
  15528. },
  15529. graph: {
  15530. Node: Node,
  15531. Edge: Edge,
  15532. Popup: Popup,
  15533. Groups: Groups,
  15534. Images: Images
  15535. },
  15536. Timeline: Timeline,
  15537. Graph: Graph
  15538. };
  15539. /**
  15540. * CommonJS module exports
  15541. */
  15542. if (typeof exports !== 'undefined') {
  15543. exports = vis;
  15544. }
  15545. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  15546. module.exports = vis;
  15547. }
  15548. /**
  15549. * AMD module exports
  15550. */
  15551. if (typeof(define) === 'function') {
  15552. define(function () {
  15553. return vis;
  15554. });
  15555. }
  15556. /**
  15557. * Window exports
  15558. */
  15559. if (typeof window !== 'undefined') {
  15560. // attach the module to the window, load as a regular javascript file
  15561. window['vis'] = vis;
  15562. }
  15563. },{"hammerjs":1,"moment":2,"mouseTrap":3}]},{},[4])
  15564. (4)
  15565. });