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.

14790 lines
448 KiB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
  1. /**
  2. * vis.js
  3. * https://github.com/almende/vis
  4. *
  5. * A dynamic, browser-based visualization library.
  6. *
  7. <<<<<<< HEAD
  8. * @version 0.2.0-SNAPSHOT
  9. * @date 2013-08-30
  10. =======
  11. * @version 0.2.0
  12. * @date 2013-09-20
  13. >>>>>>> upstream/develop
  14. *
  15. * @license
  16. * Copyright (C) 2011-2013 Almende B.V, http://almende.com
  17. *
  18. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  19. * use this file except in compliance with the License. You may obtain a copy
  20. * of the License at
  21. *
  22. * http://www.apache.org/licenses/LICENSE-2.0
  23. *
  24. * Unless required by applicable law or agreed to in writing, software
  25. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  26. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  27. * License for the specific language governing permissions and limitations under
  28. * the License.
  29. */
  30. (function(e){if("function"==typeof bootstrap)bootstrap("vis",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeVis=e}else"undefined"!=typeof window?window.vis=e():global.vis=e()})(function(){var define,ses,bootstrap,module,exports;
  31. 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){
  32. /*! Hammer.JS - v1.0.5 - 2013-04-07
  33. * http://eightmedia.github.com/hammer.js
  34. *
  35. * Copyright (c) 2013 Jorik Tangelder <j.tangelder@gmail.com>;
  36. * Licensed under the MIT license */
  37. (function(window, undefined) {
  38. 'use strict';
  39. /**
  40. * Hammer
  41. * use this to create instances
  42. * @param {HTMLElement} element
  43. * @param {Object} options
  44. * @returns {Hammer.Instance}
  45. * @constructor
  46. */
  47. var Hammer = function(element, options) {
  48. return new Hammer.Instance(element, options || {});
  49. };
  50. // default settings
  51. Hammer.defaults = {
  52. // add styles and attributes to the element to prevent the browser from doing
  53. // its native behavior. this doesnt prevent the scrolling, but cancels
  54. // the contextmenu, tap highlighting etc
  55. // set to false to disable this
  56. stop_browser_behavior: {
  57. // this also triggers onselectstart=false for IE
  58. userSelect: 'none',
  59. // this makes the element blocking in IE10 >, you could experiment with the value
  60. // see for more options this issue; https://github.com/EightMedia/hammer.js/issues/241
  61. touchAction: 'none',
  62. touchCallout: 'none',
  63. contentZooming: 'none',
  64. userDrag: 'none',
  65. tapHighlightColor: 'rgba(0,0,0,0)'
  66. }
  67. // more settings are defined per gesture at gestures.js
  68. };
  69. // detect touchevents
  70. Hammer.HAS_POINTEREVENTS = navigator.pointerEnabled || navigator.msPointerEnabled;
  71. Hammer.HAS_TOUCHEVENTS = ('ontouchstart' in window);
  72. // dont use mouseevents on mobile devices
  73. Hammer.MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;
  74. Hammer.NO_MOUSEEVENTS = Hammer.HAS_TOUCHEVENTS && navigator.userAgent.match(Hammer.MOBILE_REGEX);
  75. // eventtypes per touchevent (start, move, end)
  76. // are filled by Hammer.event.determineEventTypes on setup
  77. Hammer.EVENT_TYPES = {};
  78. // direction defines
  79. Hammer.DIRECTION_DOWN = 'down';
  80. Hammer.DIRECTION_LEFT = 'left';
  81. Hammer.DIRECTION_UP = 'up';
  82. Hammer.DIRECTION_RIGHT = 'right';
  83. // pointer type
  84. Hammer.POINTER_MOUSE = 'mouse';
  85. Hammer.POINTER_TOUCH = 'touch';
  86. Hammer.POINTER_PEN = 'pen';
  87. // touch event defines
  88. Hammer.EVENT_START = 'start';
  89. Hammer.EVENT_MOVE = 'move';
  90. Hammer.EVENT_END = 'end';
  91. // hammer document where the base events are added at
  92. Hammer.DOCUMENT = document;
  93. // plugins namespace
  94. Hammer.plugins = {};
  95. // if the window events are set...
  96. Hammer.READY = false;
  97. /**
  98. * setup events to detect gestures on the document
  99. */
  100. function setup() {
  101. if(Hammer.READY) {
  102. return;
  103. }
  104. // find what eventtypes we add listeners to
  105. Hammer.event.determineEventTypes();
  106. // Register all gestures inside Hammer.gestures
  107. for(var name in Hammer.gestures) {
  108. if(Hammer.gestures.hasOwnProperty(name)) {
  109. Hammer.detection.register(Hammer.gestures[name]);
  110. }
  111. }
  112. // Add touch events on the document
  113. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_MOVE, Hammer.detection.detect);
  114. Hammer.event.onTouch(Hammer.DOCUMENT, Hammer.EVENT_END, Hammer.detection.detect);
  115. // Hammer is ready...!
  116. Hammer.READY = true;
  117. }
  118. /**
  119. * create new hammer instance
  120. * all methods should return the instance itself, so it is chainable.
  121. * @param {HTMLElement} element
  122. * @param {Object} [options={}]
  123. * @returns {Hammer.Instance}
  124. * @constructor
  125. */
  126. Hammer.Instance = function(element, options) {
  127. var self = this;
  128. // setup HammerJS window events and register all gestures
  129. // this also sets up the default options
  130. setup();
  131. this.element = element;
  132. // start/stop detection option
  133. this.enabled = true;
  134. // merge options
  135. this.options = Hammer.utils.extend(
  136. Hammer.utils.extend({}, Hammer.defaults),
  137. options || {});
  138. // add some css to the element to prevent the browser from doing its native behavoir
  139. if(this.options.stop_browser_behavior) {
  140. Hammer.utils.stopDefaultBrowserBehavior(this.element, this.options.stop_browser_behavior);
  141. }
  142. // start detection on touchstart
  143. Hammer.event.onTouch(element, Hammer.EVENT_START, function(ev) {
  144. if(self.enabled) {
  145. Hammer.detection.startDetect(self, ev);
  146. }
  147. });
  148. // return instance
  149. return this;
  150. };
  151. Hammer.Instance.prototype = {
  152. /**
  153. * bind events to the instance
  154. * @param {String} gesture
  155. * @param {Function} handler
  156. * @returns {Hammer.Instance}
  157. */
  158. on: function onEvent(gesture, handler){
  159. var gestures = gesture.split(' ');
  160. for(var t=0; t<gestures.length; t++) {
  161. this.element.addEventListener(gestures[t], handler, false);
  162. }
  163. return this;
  164. },
  165. /**
  166. * unbind events to the instance
  167. * @param {String} gesture
  168. * @param {Function} handler
  169. * @returns {Hammer.Instance}
  170. */
  171. off: function offEvent(gesture, handler){
  172. var gestures = gesture.split(' ');
  173. for(var t=0; t<gestures.length; t++) {
  174. this.element.removeEventListener(gestures[t], handler, false);
  175. }
  176. return this;
  177. },
  178. /**
  179. * trigger gesture event
  180. * @param {String} gesture
  181. * @param {Object} eventData
  182. * @returns {Hammer.Instance}
  183. */
  184. trigger: function triggerEvent(gesture, eventData){
  185. // create DOM event
  186. var event = Hammer.DOCUMENT.createEvent('Event');
  187. event.initEvent(gesture, true, true);
  188. event.gesture = eventData;
  189. // trigger on the target if it is in the instance element,
  190. // this is for event delegation tricks
  191. var element = this.element;
  192. if(Hammer.utils.hasParent(eventData.target, element)) {
  193. element = eventData.target;
  194. }
  195. element.dispatchEvent(event);
  196. return this;
  197. },
  198. /**
  199. * enable of disable hammer.js detection
  200. * @param {Boolean} state
  201. * @returns {Hammer.Instance}
  202. */
  203. enable: function enable(state) {
  204. this.enabled = state;
  205. return this;
  206. }
  207. };
  208. /**
  209. * this holds the last move event,
  210. * used to fix empty touchend issue
  211. * see the onTouch event for an explanation
  212. * @type {Object}
  213. */
  214. var last_move_event = null;
  215. /**
  216. * when the mouse is hold down, this is true
  217. * @type {Boolean}
  218. */
  219. var enable_detect = false;
  220. /**
  221. * when touch events have been fired, this is true
  222. * @type {Boolean}
  223. */
  224. var touch_triggered = false;
  225. Hammer.event = {
  226. /**
  227. * simple addEventListener
  228. * @param {HTMLElement} element
  229. * @param {String} type
  230. * @param {Function} handler
  231. */
  232. bindDom: function(element, type, handler) {
  233. var types = type.split(' ');
  234. for(var t=0; t<types.length; t++) {
  235. element.addEventListener(types[t], handler, false);
  236. }
  237. },
  238. /**
  239. * touch events with mouse fallback
  240. * @param {HTMLElement} element
  241. * @param {String} eventType like Hammer.EVENT_MOVE
  242. * @param {Function} handler
  243. */
  244. onTouch: function onTouch(element, eventType, handler) {
  245. var self = this;
  246. this.bindDom(element, Hammer.EVENT_TYPES[eventType], function bindDomOnTouch(ev) {
  247. var sourceEventType = ev.type.toLowerCase();
  248. // onmouseup, but when touchend has been fired we do nothing.
  249. // this is for touchdevices which also fire a mouseup on touchend
  250. if(sourceEventType.match(/mouse/) && touch_triggered) {
  251. return;
  252. }
  253. // mousebutton must be down or a touch event
  254. else if( sourceEventType.match(/touch/) || // touch events are always on screen
  255. sourceEventType.match(/pointerdown/) || // pointerevents touch
  256. (sourceEventType.match(/mouse/) && ev.which === 1) // mouse is pressed
  257. ){
  258. enable_detect = true;
  259. }
  260. // we are in a touch event, set the touch triggered bool to true,
  261. // this for the conflicts that may occur on ios and android
  262. if(sourceEventType.match(/touch|pointer/)) {
  263. touch_triggered = true;
  264. }
  265. // count the total touches on the screen
  266. var count_touches = 0;
  267. // when touch has been triggered in this detection session
  268. // and we are now handling a mouse event, we stop that to prevent conflicts
  269. if(enable_detect) {
  270. // update pointerevent
  271. if(Hammer.HAS_POINTEREVENTS && eventType != Hammer.EVENT_END) {
  272. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  273. }
  274. // touch
  275. else if(sourceEventType.match(/touch/)) {
  276. count_touches = ev.touches.length;
  277. }
  278. // mouse
  279. else if(!touch_triggered) {
  280. count_touches = sourceEventType.match(/up/) ? 0 : 1;
  281. }
  282. // if we are in a end event, but when we remove one touch and
  283. // we still have enough, set eventType to move
  284. if(count_touches > 0 && eventType == Hammer.EVENT_END) {
  285. eventType = Hammer.EVENT_MOVE;
  286. }
  287. // no touches, force the end event
  288. else if(!count_touches) {
  289. eventType = Hammer.EVENT_END;
  290. }
  291. // because touchend has no touches, and we often want to use these in our gestures,
  292. // we send the last move event as our eventData in touchend
  293. if(!count_touches && last_move_event !== null) {
  294. ev = last_move_event;
  295. }
  296. // store the last move event
  297. else {
  298. last_move_event = ev;
  299. }
  300. // trigger the handler
  301. handler.call(Hammer.detection, self.collectEventData(element, eventType, ev));
  302. // remove pointerevent from list
  303. if(Hammer.HAS_POINTEREVENTS && eventType == Hammer.EVENT_END) {
  304. count_touches = Hammer.PointerEvent.updatePointer(eventType, ev);
  305. }
  306. }
  307. //debug(sourceEventType +" "+ eventType);
  308. // on the end we reset everything
  309. if(!count_touches) {
  310. last_move_event = null;
  311. enable_detect = false;
  312. touch_triggered = false;
  313. Hammer.PointerEvent.reset();
  314. }
  315. });
  316. },
  317. /**
  318. * we have different events for each device/browser
  319. * determine what we need and set them in the Hammer.EVENT_TYPES constant
  320. */
  321. determineEventTypes: function determineEventTypes() {
  322. // determine the eventtype we want to set
  323. var types;
  324. // pointerEvents magic
  325. if(Hammer.HAS_POINTEREVENTS) {
  326. types = Hammer.PointerEvent.getEvents();
  327. }
  328. // on Android, iOS, blackberry, windows mobile we dont want any mouseevents
  329. else if(Hammer.NO_MOUSEEVENTS) {
  330. types = [
  331. 'touchstart',
  332. 'touchmove',
  333. 'touchend touchcancel'];
  334. }
  335. // for non pointer events browsers and mixed browsers,
  336. // like chrome on windows8 touch laptop
  337. else {
  338. types = [
  339. 'touchstart mousedown',
  340. 'touchmove mousemove',
  341. 'touchend touchcancel mouseup'];
  342. }
  343. Hammer.EVENT_TYPES[Hammer.EVENT_START] = types[0];
  344. Hammer.EVENT_TYPES[Hammer.EVENT_MOVE] = types[1];
  345. Hammer.EVENT_TYPES[Hammer.EVENT_END] = types[2];
  346. },
  347. /**
  348. * create touchlist depending on the event
  349. * @param {Object} ev
  350. * @param {String} eventType used by the fakemultitouch plugin
  351. */
  352. getTouchList: function getTouchList(ev/*, eventType*/) {
  353. // get the fake pointerEvent touchlist
  354. if(Hammer.HAS_POINTEREVENTS) {
  355. return Hammer.PointerEvent.getTouchList();
  356. }
  357. // get the touchlist
  358. else if(ev.touches) {
  359. return ev.touches;
  360. }
  361. // make fake touchlist from mouse position
  362. else {
  363. return [{
  364. identifier: 1,
  365. pageX: ev.pageX,
  366. pageY: ev.pageY,
  367. target: ev.target
  368. }];
  369. }
  370. },
  371. /**
  372. * collect event data for Hammer js
  373. * @param {HTMLElement} element
  374. * @param {String} eventType like Hammer.EVENT_MOVE
  375. * @param {Object} eventData
  376. */
  377. collectEventData: function collectEventData(element, eventType, ev) {
  378. var touches = this.getTouchList(ev, eventType);
  379. // find out pointerType
  380. var pointerType = Hammer.POINTER_TOUCH;
  381. if(ev.type.match(/mouse/) || Hammer.PointerEvent.matchType(Hammer.POINTER_MOUSE, ev)) {
  382. pointerType = Hammer.POINTER_MOUSE;
  383. }
  384. return {
  385. center : Hammer.utils.getCenter(touches),
  386. timeStamp : new Date().getTime(),
  387. target : ev.target,
  388. touches : touches,
  389. eventType : eventType,
  390. pointerType : pointerType,
  391. srcEvent : ev,
  392. /**
  393. * prevent the browser default actions
  394. * mostly used to disable scrolling of the browser
  395. */
  396. preventDefault: function() {
  397. if(this.srcEvent.preventManipulation) {
  398. this.srcEvent.preventManipulation();
  399. }
  400. if(this.srcEvent.preventDefault) {
  401. this.srcEvent.preventDefault();
  402. }
  403. },
  404. /**
  405. * stop bubbling the event up to its parents
  406. */
  407. stopPropagation: function() {
  408. this.srcEvent.stopPropagation();
  409. },
  410. /**
  411. * immediately stop gesture detection
  412. * might be useful after a swipe was detected
  413. * @return {*}
  414. */
  415. stopDetect: function() {
  416. return Hammer.detection.stopDetect();
  417. }
  418. };
  419. }
  420. };
  421. Hammer.PointerEvent = {
  422. /**
  423. * holds all pointers
  424. * @type {Object}
  425. */
  426. pointers: {},
  427. /**
  428. * get a list of pointers
  429. * @returns {Array} touchlist
  430. */
  431. getTouchList: function() {
  432. var self = this;
  433. var touchlist = [];
  434. // we can use forEach since pointerEvents only is in IE10
  435. Object.keys(self.pointers).sort().forEach(function(id) {
  436. touchlist.push(self.pointers[id]);
  437. });
  438. return touchlist;
  439. },
  440. /**
  441. * update the position of a pointer
  442. * @param {String} type Hammer.EVENT_END
  443. * @param {Object} pointerEvent
  444. */
  445. updatePointer: function(type, pointerEvent) {
  446. if(type == Hammer.EVENT_END) {
  447. this.pointers = {};
  448. }
  449. else {
  450. pointerEvent.identifier = pointerEvent.pointerId;
  451. this.pointers[pointerEvent.pointerId] = pointerEvent;
  452. }
  453. return Object.keys(this.pointers).length;
  454. },
  455. /**
  456. * check if ev matches pointertype
  457. * @param {String} pointerType Hammer.POINTER_MOUSE
  458. * @param {PointerEvent} ev
  459. */
  460. matchType: function(pointerType, ev) {
  461. if(!ev.pointerType) {
  462. return false;
  463. }
  464. var types = {};
  465. types[Hammer.POINTER_MOUSE] = (ev.pointerType == ev.MSPOINTER_TYPE_MOUSE || ev.pointerType == Hammer.POINTER_MOUSE);
  466. types[Hammer.POINTER_TOUCH] = (ev.pointerType == ev.MSPOINTER_TYPE_TOUCH || ev.pointerType == Hammer.POINTER_TOUCH);
  467. types[Hammer.POINTER_PEN] = (ev.pointerType == ev.MSPOINTER_TYPE_PEN || ev.pointerType == Hammer.POINTER_PEN);
  468. return types[pointerType];
  469. },
  470. /**
  471. * get events
  472. */
  473. getEvents: function() {
  474. return [
  475. 'pointerdown MSPointerDown',
  476. 'pointermove MSPointerMove',
  477. 'pointerup pointercancel MSPointerUp MSPointerCancel'
  478. ];
  479. },
  480. /**
  481. * reset the list
  482. */
  483. reset: function() {
  484. this.pointers = {};
  485. }
  486. };
  487. Hammer.utils = {
  488. /**
  489. * extend method,
  490. * also used for cloning when dest is an empty object
  491. * @param {Object} dest
  492. * @param {Object} src
  493. * @parm {Boolean} merge do a merge
  494. * @returns {Object} dest
  495. */
  496. extend: function extend(dest, src, merge) {
  497. for (var key in src) {
  498. if(dest[key] !== undefined && merge) {
  499. continue;
  500. }
  501. dest[key] = src[key];
  502. }
  503. return dest;
  504. },
  505. /**
  506. * find if a node is in the given parent
  507. * used for event delegation tricks
  508. * @param {HTMLElement} node
  509. * @param {HTMLElement} parent
  510. * @returns {boolean} has_parent
  511. */
  512. hasParent: function(node, parent) {
  513. while(node){
  514. if(node == parent) {
  515. return true;
  516. }
  517. node = node.parentNode;
  518. }
  519. return false;
  520. },
  521. /**
  522. * get the center of all the touches
  523. * @param {Array} touches
  524. * @returns {Object} center
  525. */
  526. getCenter: function getCenter(touches) {
  527. var valuesX = [], valuesY = [];
  528. for(var t= 0,len=touches.length; t<len; t++) {
  529. valuesX.push(touches[t].pageX);
  530. valuesY.push(touches[t].pageY);
  531. }
  532. return {
  533. pageX: ((Math.min.apply(Math, valuesX) + Math.max.apply(Math, valuesX)) / 2),
  534. pageY: ((Math.min.apply(Math, valuesY) + Math.max.apply(Math, valuesY)) / 2)
  535. };
  536. },
  537. /**
  538. * calculate the velocity between two points
  539. * @param {Number} delta_time
  540. * @param {Number} delta_x
  541. * @param {Number} delta_y
  542. * @returns {Object} velocity
  543. */
  544. getVelocity: function getVelocity(delta_time, delta_x, delta_y) {
  545. return {
  546. x: Math.abs(delta_x / delta_time) || 0,
  547. y: Math.abs(delta_y / delta_time) || 0
  548. };
  549. },
  550. /**
  551. * calculate the angle between two coordinates
  552. * @param {Touch} touch1
  553. * @param {Touch} touch2
  554. * @returns {Number} angle
  555. */
  556. getAngle: function getAngle(touch1, touch2) {
  557. var y = touch2.pageY - touch1.pageY,
  558. x = touch2.pageX - touch1.pageX;
  559. return Math.atan2(y, x) * 180 / Math.PI;
  560. },
  561. /**
  562. * angle to direction define
  563. * @param {Touch} touch1
  564. * @param {Touch} touch2
  565. * @returns {String} direction constant, like Hammer.DIRECTION_LEFT
  566. */
  567. getDirection: function getDirection(touch1, touch2) {
  568. var x = Math.abs(touch1.pageX - touch2.pageX),
  569. y = Math.abs(touch1.pageY - touch2.pageY);
  570. if(x >= y) {
  571. return touch1.pageX - touch2.pageX > 0 ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  572. }
  573. else {
  574. return touch1.pageY - touch2.pageY > 0 ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  575. }
  576. },
  577. /**
  578. * calculate the distance between two touches
  579. * @param {Touch} touch1
  580. * @param {Touch} touch2
  581. * @returns {Number} distance
  582. */
  583. getDistance: function getDistance(touch1, touch2) {
  584. var x = touch2.pageX - touch1.pageX,
  585. y = touch2.pageY - touch1.pageY;
  586. return Math.sqrt((x*x) + (y*y));
  587. },
  588. /**
  589. * calculate the scale factor between two touchLists (fingers)
  590. * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
  591. * @param {Array} start
  592. * @param {Array} end
  593. * @returns {Number} scale
  594. */
  595. getScale: function getScale(start, end) {
  596. // need two fingers...
  597. if(start.length >= 2 && end.length >= 2) {
  598. return this.getDistance(end[0], end[1]) /
  599. this.getDistance(start[0], start[1]);
  600. }
  601. return 1;
  602. },
  603. /**
  604. * calculate the rotation degrees between two touchLists (fingers)
  605. * @param {Array} start
  606. * @param {Array} end
  607. * @returns {Number} rotation
  608. */
  609. getRotation: function getRotation(start, end) {
  610. // need two fingers
  611. if(start.length >= 2 && end.length >= 2) {
  612. return this.getAngle(end[1], end[0]) -
  613. this.getAngle(start[1], start[0]);
  614. }
  615. return 0;
  616. },
  617. /**
  618. * boolean if the direction is vertical
  619. * @param {String} direction
  620. * @returns {Boolean} is_vertical
  621. */
  622. isVertical: function isVertical(direction) {
  623. return (direction == Hammer.DIRECTION_UP || direction == Hammer.DIRECTION_DOWN);
  624. },
  625. /**
  626. * stop browser default behavior with css props
  627. * @param {HtmlElement} element
  628. * @param {Object} css_props
  629. */
  630. stopDefaultBrowserBehavior: function stopDefaultBrowserBehavior(element, css_props) {
  631. var prop,
  632. vendors = ['webkit','khtml','moz','ms','o',''];
  633. if(!css_props || !element.style) {
  634. return;
  635. }
  636. // with css properties for modern browsers
  637. for(var i = 0; i < vendors.length; i++) {
  638. for(var p in css_props) {
  639. if(css_props.hasOwnProperty(p)) {
  640. prop = p;
  641. // vender prefix at the property
  642. if(vendors[i]) {
  643. prop = vendors[i] + prop.substring(0, 1).toUpperCase() + prop.substring(1);
  644. }
  645. // set the style
  646. element.style[prop] = css_props[p];
  647. }
  648. }
  649. }
  650. // also the disable onselectstart
  651. if(css_props.userSelect == 'none') {
  652. element.onselectstart = function() {
  653. return false;
  654. };
  655. }
  656. }
  657. };
  658. Hammer.detection = {
  659. // contains all registred Hammer.gestures in the correct order
  660. gestures: [],
  661. // data of the current Hammer.gesture detection session
  662. current: null,
  663. // the previous Hammer.gesture session data
  664. // is a full clone of the previous gesture.current object
  665. previous: null,
  666. // when this becomes true, no gestures are fired
  667. stopped: false,
  668. /**
  669. * start Hammer.gesture detection
  670. * @param {Hammer.Instance} inst
  671. * @param {Object} eventData
  672. */
  673. startDetect: function startDetect(inst, eventData) {
  674. // already busy with a Hammer.gesture detection on an element
  675. if(this.current) {
  676. return;
  677. }
  678. this.stopped = false;
  679. this.current = {
  680. inst : inst, // reference to HammerInstance we're working for
  681. startEvent : Hammer.utils.extend({}, eventData), // start eventData for distances, timing etc
  682. lastEvent : false, // last eventData
  683. name : '' // current gesture we're in/detected, can be 'tap', 'hold' etc
  684. };
  685. this.detect(eventData);
  686. },
  687. /**
  688. * Hammer.gesture detection
  689. * @param {Object} eventData
  690. * @param {Object} eventData
  691. */
  692. detect: function detect(eventData) {
  693. if(!this.current || this.stopped) {
  694. return;
  695. }
  696. // extend event data with calculations about scale, distance etc
  697. eventData = this.extendEventData(eventData);
  698. // instance options
  699. var inst_options = this.current.inst.options;
  700. // call Hammer.gesture handlers
  701. for(var g=0,len=this.gestures.length; g<len; g++) {
  702. var gesture = this.gestures[g];
  703. // only when the instance options have enabled this gesture
  704. if(!this.stopped && inst_options[gesture.name] !== false) {
  705. // if a handler returns false, we stop with the detection
  706. if(gesture.handler.call(gesture, eventData, this.current.inst) === false) {
  707. this.stopDetect();
  708. break;
  709. }
  710. }
  711. }
  712. // store as previous event event
  713. if(this.current) {
  714. this.current.lastEvent = eventData;
  715. }
  716. // endevent, but not the last touch, so dont stop
  717. if(eventData.eventType == Hammer.EVENT_END && !eventData.touches.length-1) {
  718. this.stopDetect();
  719. }
  720. return eventData;
  721. },
  722. /**
  723. * clear the Hammer.gesture vars
  724. * this is called on endDetect, but can also be used when a final Hammer.gesture has been detected
  725. * to stop other Hammer.gestures from being fired
  726. */
  727. stopDetect: function stopDetect() {
  728. // clone current data to the store as the previous gesture
  729. // used for the double tap gesture, since this is an other gesture detect session
  730. this.previous = Hammer.utils.extend({}, this.current);
  731. // reset the current
  732. this.current = null;
  733. // stopped!
  734. this.stopped = true;
  735. },
  736. /**
  737. * extend eventData for Hammer.gestures
  738. * @param {Object} ev
  739. * @returns {Object} ev
  740. */
  741. extendEventData: function extendEventData(ev) {
  742. var startEv = this.current.startEvent;
  743. // if the touches change, set the new touches over the startEvent touches
  744. // this because touchevents don't have all the touches on touchstart, or the
  745. // user must place his fingers at the EXACT same time on the screen, which is not realistic
  746. // but, sometimes it happens that both fingers are touching at the EXACT same time
  747. if(startEv && (ev.touches.length != startEv.touches.length || ev.touches === startEv.touches)) {
  748. // extend 1 level deep to get the touchlist with the touch objects
  749. startEv.touches = [];
  750. for(var i=0,len=ev.touches.length; i<len; i++) {
  751. startEv.touches.push(Hammer.utils.extend({}, ev.touches[i]));
  752. }
  753. }
  754. var delta_time = ev.timeStamp - startEv.timeStamp,
  755. delta_x = ev.center.pageX - startEv.center.pageX,
  756. delta_y = ev.center.pageY - startEv.center.pageY,
  757. velocity = Hammer.utils.getVelocity(delta_time, delta_x, delta_y);
  758. Hammer.utils.extend(ev, {
  759. deltaTime : delta_time,
  760. deltaX : delta_x,
  761. deltaY : delta_y,
  762. velocityX : velocity.x,
  763. velocityY : velocity.y,
  764. distance : Hammer.utils.getDistance(startEv.center, ev.center),
  765. angle : Hammer.utils.getAngle(startEv.center, ev.center),
  766. direction : Hammer.utils.getDirection(startEv.center, ev.center),
  767. scale : Hammer.utils.getScale(startEv.touches, ev.touches),
  768. rotation : Hammer.utils.getRotation(startEv.touches, ev.touches),
  769. startEvent : startEv
  770. });
  771. return ev;
  772. },
  773. /**
  774. * register new gesture
  775. * @param {Object} gesture object, see gestures.js for documentation
  776. * @returns {Array} gestures
  777. */
  778. register: function register(gesture) {
  779. // add an enable gesture options if there is no given
  780. var options = gesture.defaults || {};
  781. if(options[gesture.name] === undefined) {
  782. options[gesture.name] = true;
  783. }
  784. // extend Hammer default options with the Hammer.gesture options
  785. Hammer.utils.extend(Hammer.defaults, options, true);
  786. // set its index
  787. gesture.index = gesture.index || 1000;
  788. // add Hammer.gesture to the list
  789. this.gestures.push(gesture);
  790. // sort the list by index
  791. this.gestures.sort(function(a, b) {
  792. if (a.index < b.index) {
  793. return -1;
  794. }
  795. if (a.index > b.index) {
  796. return 1;
  797. }
  798. return 0;
  799. });
  800. return this.gestures;
  801. }
  802. };
  803. Hammer.gestures = Hammer.gestures || {};
  804. /**
  805. * Custom gestures
  806. * ==============================
  807. *
  808. * Gesture object
  809. * --------------------
  810. * The object structure of a gesture:
  811. *
  812. * { name: 'mygesture',
  813. * index: 1337,
  814. * defaults: {
  815. * mygesture_option: true
  816. * }
  817. * handler: function(type, ev, inst) {
  818. * // trigger gesture event
  819. * inst.trigger(this.name, ev);
  820. * }
  821. * }
  822. * @param {String} name
  823. * this should be the name of the gesture, lowercase
  824. * it is also being used to disable/enable the gesture per instance config.
  825. *
  826. * @param {Number} [index=1000]
  827. * the index of the gesture, where it is going to be in the stack of gestures detection
  828. * like when you build an gesture that depends on the drag gesture, it is a good
  829. * idea to place it after the index of the drag gesture.
  830. *
  831. * @param {Object} [defaults={}]
  832. * the default settings of the gesture. these are added to the instance settings,
  833. * and can be overruled per instance. you can also add the name of the gesture,
  834. * but this is also added by default (and set to true).
  835. *
  836. * @param {Function} handler
  837. * this handles the gesture detection of your custom gesture and receives the
  838. * following arguments:
  839. *
  840. * @param {Object} eventData
  841. * event data containing the following properties:
  842. * timeStamp {Number} time the event occurred
  843. * target {HTMLElement} target element
  844. * touches {Array} touches (fingers, pointers, mouse) on the screen
  845. * pointerType {String} kind of pointer that was used. matches Hammer.POINTER_MOUSE|TOUCH
  846. * center {Object} center position of the touches. contains pageX and pageY
  847. * deltaTime {Number} the total time of the touches in the screen
  848. * deltaX {Number} the delta on x axis we haved moved
  849. * deltaY {Number} the delta on y axis we haved moved
  850. * velocityX {Number} the velocity on the x
  851. * velocityY {Number} the velocity on y
  852. * angle {Number} the angle we are moving
  853. * direction {String} the direction we are moving. matches Hammer.DIRECTION_UP|DOWN|LEFT|RIGHT
  854. * distance {Number} the distance we haved moved
  855. * scale {Number} scaling of the touches, needs 2 touches
  856. * rotation {Number} rotation of the touches, needs 2 touches *
  857. * eventType {String} matches Hammer.EVENT_START|MOVE|END
  858. * srcEvent {Object} the source event, like TouchStart or MouseDown *
  859. * startEvent {Object} contains the same properties as above,
  860. * but from the first touch. this is used to calculate
  861. * distances, deltaTime, scaling etc
  862. *
  863. * @param {Hammer.Instance} inst
  864. * the instance we are doing the detection for. you can get the options from
  865. * the inst.options object and trigger the gesture event by calling inst.trigger
  866. *
  867. *
  868. * Handle gestures
  869. * --------------------
  870. * inside the handler you can get/set Hammer.detection.current. This is the current
  871. * detection session. It has the following properties
  872. * @param {String} name
  873. * contains the name of the gesture we have detected. it has not a real function,
  874. * only to check in other gestures if something is detected.
  875. * like in the drag gesture we set it to 'drag' and in the swipe gesture we can
  876. * check if the current gesture is 'drag' by accessing Hammer.detection.current.name
  877. *
  878. * @readonly
  879. * @param {Hammer.Instance} inst
  880. * the instance we do the detection for
  881. *
  882. * @readonly
  883. * @param {Object} startEvent
  884. * contains the properties of the first gesture detection in this session.
  885. * Used for calculations about timing, distance, etc.
  886. *
  887. * @readonly
  888. * @param {Object} lastEvent
  889. * contains all the properties of the last gesture detect in this session.
  890. *
  891. * after the gesture detection session has been completed (user has released the screen)
  892. * the Hammer.detection.current object is copied into Hammer.detection.previous,
  893. * this is usefull for gestures like doubletap, where you need to know if the
  894. * previous gesture was a tap
  895. *
  896. * options that have been set by the instance can be received by calling inst.options
  897. *
  898. * You can trigger a gesture event by calling inst.trigger("mygesture", event).
  899. * The first param is the name of your gesture, the second the event argument
  900. *
  901. *
  902. * Register gestures
  903. * --------------------
  904. * When an gesture is added to the Hammer.gestures object, it is auto registered
  905. * at the setup of the first Hammer instance. You can also call Hammer.detection.register
  906. * manually and pass your gesture object as a param
  907. *
  908. */
  909. /**
  910. * Hold
  911. * Touch stays at the same place for x time
  912. * @events hold
  913. */
  914. Hammer.gestures.Hold = {
  915. name: 'hold',
  916. index: 10,
  917. defaults: {
  918. hold_timeout : 500,
  919. hold_threshold : 1
  920. },
  921. timer: null,
  922. handler: function holdGesture(ev, inst) {
  923. switch(ev.eventType) {
  924. case Hammer.EVENT_START:
  925. // clear any running timers
  926. clearTimeout(this.timer);
  927. // set the gesture so we can check in the timeout if it still is
  928. Hammer.detection.current.name = this.name;
  929. // set timer and if after the timeout it still is hold,
  930. // we trigger the hold event
  931. this.timer = setTimeout(function() {
  932. if(Hammer.detection.current.name == 'hold') {
  933. inst.trigger('hold', ev);
  934. }
  935. }, inst.options.hold_timeout);
  936. break;
  937. // when you move or end we clear the timer
  938. case Hammer.EVENT_MOVE:
  939. if(ev.distance > inst.options.hold_threshold) {
  940. clearTimeout(this.timer);
  941. }
  942. break;
  943. case Hammer.EVENT_END:
  944. clearTimeout(this.timer);
  945. break;
  946. }
  947. }
  948. };
  949. /**
  950. * Tap/DoubleTap
  951. * Quick touch at a place or double at the same place
  952. * @events tap, doubletap
  953. */
  954. Hammer.gestures.Tap = {
  955. name: 'tap',
  956. index: 100,
  957. defaults: {
  958. tap_max_touchtime : 250,
  959. tap_max_distance : 10,
  960. tap_always : true,
  961. doubletap_distance : 20,
  962. doubletap_interval : 300
  963. },
  964. handler: function tapGesture(ev, inst) {
  965. if(ev.eventType == Hammer.EVENT_END) {
  966. // previous gesture, for the double tap since these are two different gesture detections
  967. var prev = Hammer.detection.previous,
  968. did_doubletap = false;
  969. // when the touchtime is higher then the max touch time
  970. // or when the moving distance is too much
  971. if(ev.deltaTime > inst.options.tap_max_touchtime ||
  972. ev.distance > inst.options.tap_max_distance) {
  973. return;
  974. }
  975. // check if double tap
  976. if(prev && prev.name == 'tap' &&
  977. (ev.timeStamp - prev.lastEvent.timeStamp) < inst.options.doubletap_interval &&
  978. ev.distance < inst.options.doubletap_distance) {
  979. inst.trigger('doubletap', ev);
  980. did_doubletap = true;
  981. }
  982. // do a single tap
  983. if(!did_doubletap || inst.options.tap_always) {
  984. Hammer.detection.current.name = 'tap';
  985. inst.trigger(Hammer.detection.current.name, ev);
  986. }
  987. }
  988. }
  989. };
  990. /**
  991. * Swipe
  992. * triggers swipe events when the end velocity is above the threshold
  993. * @events swipe, swipeleft, swiperight, swipeup, swipedown
  994. */
  995. Hammer.gestures.Swipe = {
  996. name: 'swipe',
  997. index: 40,
  998. defaults: {
  999. // set 0 for unlimited, but this can conflict with transform
  1000. swipe_max_touches : 1,
  1001. swipe_velocity : 0.7
  1002. },
  1003. handler: function swipeGesture(ev, inst) {
  1004. if(ev.eventType == Hammer.EVENT_END) {
  1005. // max touches
  1006. if(inst.options.swipe_max_touches > 0 &&
  1007. ev.touches.length > inst.options.swipe_max_touches) {
  1008. return;
  1009. }
  1010. // when the distance we moved is too small we skip this gesture
  1011. // or we can be already in dragging
  1012. if(ev.velocityX > inst.options.swipe_velocity ||
  1013. ev.velocityY > inst.options.swipe_velocity) {
  1014. // trigger swipe events
  1015. inst.trigger(this.name, ev);
  1016. inst.trigger(this.name + ev.direction, ev);
  1017. }
  1018. }
  1019. }
  1020. };
  1021. /**
  1022. * Drag
  1023. * Move with x fingers (default 1) around on the page. Blocking the scrolling when
  1024. * moving left and right is a good practice. When all the drag events are blocking
  1025. * you disable scrolling on that area.
  1026. * @events drag, drapleft, dragright, dragup, dragdown
  1027. */
  1028. Hammer.gestures.Drag = {
  1029. name: 'drag',
  1030. index: 50,
  1031. defaults: {
  1032. drag_min_distance : 10,
  1033. // set 0 for unlimited, but this can conflict with transform
  1034. drag_max_touches : 1,
  1035. // prevent default browser behavior when dragging occurs
  1036. // be careful with it, it makes the element a blocking element
  1037. // when you are using the drag gesture, it is a good practice to set this true
  1038. drag_block_horizontal : false,
  1039. drag_block_vertical : false,
  1040. // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
  1041. // It disallows vertical directions if the initial direction was horizontal, and vice versa.
  1042. drag_lock_to_axis : false,
  1043. // drag lock only kicks in when distance > drag_lock_min_distance
  1044. // This way, locking occurs only when the distance has become large enough to reliably determine the direction
  1045. drag_lock_min_distance : 25
  1046. },
  1047. triggered: false,
  1048. handler: function dragGesture(ev, inst) {
  1049. // current gesture isnt drag, but dragged is true
  1050. // this means an other gesture is busy. now call dragend
  1051. if(Hammer.detection.current.name != this.name && this.triggered) {
  1052. inst.trigger(this.name +'end', ev);
  1053. this.triggered = false;
  1054. return;
  1055. }
  1056. // max touches
  1057. if(inst.options.drag_max_touches > 0 &&
  1058. ev.touches.length > inst.options.drag_max_touches) {
  1059. return;
  1060. }
  1061. switch(ev.eventType) {
  1062. case Hammer.EVENT_START:
  1063. this.triggered = false;
  1064. break;
  1065. case Hammer.EVENT_MOVE:
  1066. // when the distance we moved is too small we skip this gesture
  1067. // or we can be already in dragging
  1068. if(ev.distance < inst.options.drag_min_distance &&
  1069. Hammer.detection.current.name != this.name) {
  1070. return;
  1071. }
  1072. // we are dragging!
  1073. Hammer.detection.current.name = this.name;
  1074. // lock drag to axis?
  1075. if(Hammer.detection.current.lastEvent.drag_locked_to_axis || (inst.options.drag_lock_to_axis && inst.options.drag_lock_min_distance<=ev.distance)) {
  1076. ev.drag_locked_to_axis = true;
  1077. }
  1078. var last_direction = Hammer.detection.current.lastEvent.direction;
  1079. if(ev.drag_locked_to_axis && last_direction !== ev.direction) {
  1080. // keep direction on the axis that the drag gesture started on
  1081. if(Hammer.utils.isVertical(last_direction)) {
  1082. ev.direction = (ev.deltaY < 0) ? Hammer.DIRECTION_UP : Hammer.DIRECTION_DOWN;
  1083. }
  1084. else {
  1085. ev.direction = (ev.deltaX < 0) ? Hammer.DIRECTION_LEFT : Hammer.DIRECTION_RIGHT;
  1086. }
  1087. }
  1088. // first time, trigger dragstart event
  1089. if(!this.triggered) {
  1090. inst.trigger(this.name +'start', ev);
  1091. this.triggered = true;
  1092. }
  1093. // trigger normal event
  1094. inst.trigger(this.name, ev);
  1095. // direction event, like dragdown
  1096. inst.trigger(this.name + ev.direction, ev);
  1097. // block the browser events
  1098. if( (inst.options.drag_block_vertical && Hammer.utils.isVertical(ev.direction)) ||
  1099. (inst.options.drag_block_horizontal && !Hammer.utils.isVertical(ev.direction))) {
  1100. ev.preventDefault();
  1101. }
  1102. break;
  1103. case Hammer.EVENT_END:
  1104. // trigger dragend
  1105. if(this.triggered) {
  1106. inst.trigger(this.name +'end', ev);
  1107. }
  1108. this.triggered = false;
  1109. break;
  1110. }
  1111. }
  1112. };
  1113. /**
  1114. * Transform
  1115. * User want to scale or rotate with 2 fingers
  1116. * @events transform, pinch, pinchin, pinchout, rotate
  1117. */
  1118. Hammer.gestures.Transform = {
  1119. name: 'transform',
  1120. index: 45,
  1121. defaults: {
  1122. // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
  1123. transform_min_scale : 0.01,
  1124. // rotation in degrees
  1125. transform_min_rotation : 1,
  1126. // prevent default browser behavior when two touches are on the screen
  1127. // but it makes the element a blocking element
  1128. // when you are using the transform gesture, it is a good practice to set this true
  1129. transform_always_block : false
  1130. },
  1131. triggered: false,
  1132. handler: function transformGesture(ev, inst) {
  1133. // current gesture isnt drag, but dragged is true
  1134. // this means an other gesture is busy. now call dragend
  1135. if(Hammer.detection.current.name != this.name && this.triggered) {
  1136. inst.trigger(this.name +'end', ev);
  1137. this.triggered = false;
  1138. return;
  1139. }
  1140. // atleast multitouch
  1141. if(ev.touches.length < 2) {
  1142. return;
  1143. }
  1144. // prevent default when two fingers are on the screen
  1145. if(inst.options.transform_always_block) {
  1146. ev.preventDefault();
  1147. }
  1148. switch(ev.eventType) {
  1149. case Hammer.EVENT_START:
  1150. this.triggered = false;
  1151. break;
  1152. case Hammer.EVENT_MOVE:
  1153. var scale_threshold = Math.abs(1-ev.scale);
  1154. var rotation_threshold = Math.abs(ev.rotation);
  1155. // when the distance we moved is too small we skip this gesture
  1156. // or we can be already in dragging
  1157. if(scale_threshold < inst.options.transform_min_scale &&
  1158. rotation_threshold < inst.options.transform_min_rotation) {
  1159. return;
  1160. }
  1161. // we are transforming!
  1162. Hammer.detection.current.name = this.name;
  1163. // first time, trigger dragstart event
  1164. if(!this.triggered) {
  1165. inst.trigger(this.name +'start', ev);
  1166. this.triggered = true;
  1167. }
  1168. inst.trigger(this.name, ev); // basic transform event
  1169. // trigger rotate event
  1170. if(rotation_threshold > inst.options.transform_min_rotation) {
  1171. inst.trigger('rotate', ev);
  1172. }
  1173. // trigger pinch event
  1174. if(scale_threshold > inst.options.transform_min_scale) {
  1175. inst.trigger('pinch', ev);
  1176. inst.trigger('pinch'+ ((ev.scale < 1) ? 'in' : 'out'), ev);
  1177. }
  1178. break;
  1179. case Hammer.EVENT_END:
  1180. // trigger dragend
  1181. if(this.triggered) {
  1182. inst.trigger(this.name +'end', ev);
  1183. }
  1184. this.triggered = false;
  1185. break;
  1186. }
  1187. }
  1188. };
  1189. /**
  1190. * Touch
  1191. * Called as first, tells the user has touched the screen
  1192. * @events touch
  1193. */
  1194. Hammer.gestures.Touch = {
  1195. name: 'touch',
  1196. index: -Infinity,
  1197. defaults: {
  1198. // call preventDefault at touchstart, and makes the element blocking by
  1199. // disabling the scrolling of the page, but it improves gestures like
  1200. // transforming and dragging.
  1201. // be careful with using this, it can be very annoying for users to be stuck
  1202. // on the page
  1203. prevent_default: false,
  1204. // disable mouse events, so only touch (or pen!) input triggers events
  1205. prevent_mouseevents: false
  1206. },
  1207. handler: function touchGesture(ev, inst) {
  1208. if(inst.options.prevent_mouseevents && ev.pointerType == Hammer.POINTER_MOUSE) {
  1209. ev.stopDetect();
  1210. return;
  1211. }
  1212. if(inst.options.prevent_default) {
  1213. ev.preventDefault();
  1214. }
  1215. if(ev.eventType == Hammer.EVENT_START) {
  1216. inst.trigger(this.name, ev);
  1217. }
  1218. }
  1219. };
  1220. /**
  1221. * Release
  1222. * Called as last, tells the user has released the screen
  1223. * @events release
  1224. */
  1225. Hammer.gestures.Release = {
  1226. name: 'release',
  1227. index: Infinity,
  1228. handler: function releaseGesture(ev, inst) {
  1229. if(ev.eventType == Hammer.EVENT_END) {
  1230. inst.trigger(this.name, ev);
  1231. }
  1232. }
  1233. };
  1234. // node export
  1235. if(typeof module === 'object' && typeof module.exports === 'object'){
  1236. module.exports = Hammer;
  1237. }
  1238. // just window export
  1239. else {
  1240. window.Hammer = Hammer;
  1241. // requireJS module definition
  1242. if(typeof window.define === 'function' && window.define.amd) {
  1243. window.define('hammer', [], function() {
  1244. return Hammer;
  1245. });
  1246. }
  1247. }
  1248. })(this);
  1249. },{}],2:[function(require,module,exports){
  1250. //! moment.js
  1251. //! version : 2.2.1
  1252. //! authors : Tim Wood, Iskren Chernev, Moment.js contributors
  1253. //! license : MIT
  1254. //! momentjs.com
  1255. (function (undefined) {
  1256. /************************************
  1257. Constants
  1258. ************************************/
  1259. var moment,
  1260. VERSION = "2.2.1",
  1261. round = Math.round, i,
  1262. // internal storage for language config files
  1263. languages = {},
  1264. // check for nodeJS
  1265. hasModule = (typeof module !== 'undefined' && module.exports),
  1266. // ASP.NET json date format regex
  1267. aspNetJsonRegex = /^\/?Date\((\-?\d+)/i,
  1268. aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,
  1269. // format tokens
  1270. formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,
  1271. localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,
  1272. // parsing token regexes
  1273. parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99
  1274. parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999
  1275. parseTokenThreeDigits = /\d{3}/, // 000 - 999
  1276. parseTokenFourDigits = /\d{1,4}/, // 0 - 9999
  1277. parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999
  1278. 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.
  1279. parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z
  1280. parseTokenT = /T/i, // T (ISO seperator)
  1281. parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123
  1282. // preliminary iso regex
  1283. // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000
  1284. isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,
  1285. isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
  1286. // iso time formats and regexes
  1287. isoTimes = [
  1288. ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/],
  1289. ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/],
  1290. ['HH:mm', /(T| )\d\d:\d\d/],
  1291. ['HH', /(T| )\d\d/]
  1292. ],
  1293. // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"]
  1294. parseTimezoneChunker = /([\+\-]|\d\d)/gi,
  1295. // getter and setter names
  1296. proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'),
  1297. unitMillisecondFactors = {
  1298. 'Milliseconds' : 1,
  1299. 'Seconds' : 1e3,
  1300. 'Minutes' : 6e4,
  1301. 'Hours' : 36e5,
  1302. 'Days' : 864e5,
  1303. 'Months' : 2592e6,
  1304. 'Years' : 31536e6
  1305. },
  1306. unitAliases = {
  1307. ms : 'millisecond',
  1308. s : 'second',
  1309. m : 'minute',
  1310. h : 'hour',
  1311. d : 'day',
  1312. w : 'week',
  1313. W : 'isoweek',
  1314. M : 'month',
  1315. y : 'year'
  1316. },
  1317. // format function strings
  1318. formatFunctions = {},
  1319. // tokens to ordinalize and pad
  1320. ordinalizeTokens = 'DDD w W M D d'.split(' '),
  1321. paddedTokens = 'M D H h m s w W'.split(' '),
  1322. formatTokenFunctions = {
  1323. M : function () {
  1324. return this.month() + 1;
  1325. },
  1326. MMM : function (format) {
  1327. return this.lang().monthsShort(this, format);
  1328. },
  1329. MMMM : function (format) {
  1330. return this.lang().months(this, format);
  1331. },
  1332. D : function () {
  1333. return this.date();
  1334. },
  1335. DDD : function () {
  1336. return this.dayOfYear();
  1337. },
  1338. d : function () {
  1339. return this.day();
  1340. },
  1341. dd : function (format) {
  1342. return this.lang().weekdaysMin(this, format);
  1343. },
  1344. ddd : function (format) {
  1345. return this.lang().weekdaysShort(this, format);
  1346. },
  1347. dddd : function (format) {
  1348. return this.lang().weekdays(this, format);
  1349. },
  1350. w : function () {
  1351. return this.week();
  1352. },
  1353. W : function () {
  1354. return this.isoWeek();
  1355. },
  1356. YY : function () {
  1357. return leftZeroFill(this.year() % 100, 2);
  1358. },
  1359. YYYY : function () {
  1360. return leftZeroFill(this.year(), 4);
  1361. },
  1362. YYYYY : function () {
  1363. return leftZeroFill(this.year(), 5);
  1364. },
  1365. gg : function () {
  1366. return leftZeroFill(this.weekYear() % 100, 2);
  1367. },
  1368. gggg : function () {
  1369. return this.weekYear();
  1370. },
  1371. ggggg : function () {
  1372. return leftZeroFill(this.weekYear(), 5);
  1373. },
  1374. GG : function () {
  1375. return leftZeroFill(this.isoWeekYear() % 100, 2);
  1376. },
  1377. GGGG : function () {
  1378. return this.isoWeekYear();
  1379. },
  1380. GGGGG : function () {
  1381. return leftZeroFill(this.isoWeekYear(), 5);
  1382. },
  1383. e : function () {
  1384. return this.weekday();
  1385. },
  1386. E : function () {
  1387. return this.isoWeekday();
  1388. },
  1389. a : function () {
  1390. return this.lang().meridiem(this.hours(), this.minutes(), true);
  1391. },
  1392. A : function () {
  1393. return this.lang().meridiem(this.hours(), this.minutes(), false);
  1394. },
  1395. H : function () {
  1396. return this.hours();
  1397. },
  1398. h : function () {
  1399. return this.hours() % 12 || 12;
  1400. },
  1401. m : function () {
  1402. return this.minutes();
  1403. },
  1404. s : function () {
  1405. return this.seconds();
  1406. },
  1407. S : function () {
  1408. return ~~(this.milliseconds() / 100);
  1409. },
  1410. SS : function () {
  1411. return leftZeroFill(~~(this.milliseconds() / 10), 2);
  1412. },
  1413. SSS : function () {
  1414. return leftZeroFill(this.milliseconds(), 3);
  1415. },
  1416. Z : function () {
  1417. var a = -this.zone(),
  1418. b = "+";
  1419. if (a < 0) {
  1420. a = -a;
  1421. b = "-";
  1422. }
  1423. return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2);
  1424. },
  1425. ZZ : function () {
  1426. var a = -this.zone(),
  1427. b = "+";
  1428. if (a < 0) {
  1429. a = -a;
  1430. b = "-";
  1431. }
  1432. return b + leftZeroFill(~~(10 * a / 6), 4);
  1433. },
  1434. z : function () {
  1435. return this.zoneAbbr();
  1436. },
  1437. zz : function () {
  1438. return this.zoneName();
  1439. },
  1440. X : function () {
  1441. return this.unix();
  1442. }
  1443. };
  1444. function padToken(func, count) {
  1445. return function (a) {
  1446. return leftZeroFill(func.call(this, a), count);
  1447. };
  1448. }
  1449. function ordinalizeToken(func, period) {
  1450. return function (a) {
  1451. return this.lang().ordinal(func.call(this, a), period);
  1452. };
  1453. }
  1454. while (ordinalizeTokens.length) {
  1455. i = ordinalizeTokens.pop();
  1456. formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i);
  1457. }
  1458. while (paddedTokens.length) {
  1459. i = paddedTokens.pop();
  1460. formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2);
  1461. }
  1462. formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3);
  1463. /************************************
  1464. Constructors
  1465. ************************************/
  1466. function Language() {
  1467. }
  1468. // Moment prototype object
  1469. function Moment(config) {
  1470. extend(this, config);
  1471. }
  1472. // Duration Constructor
  1473. function Duration(duration) {
  1474. var years = duration.years || duration.year || duration.y || 0,
  1475. months = duration.months || duration.month || duration.M || 0,
  1476. weeks = duration.weeks || duration.week || duration.w || 0,
  1477. days = duration.days || duration.day || duration.d || 0,
  1478. hours = duration.hours || duration.hour || duration.h || 0,
  1479. minutes = duration.minutes || duration.minute || duration.m || 0,
  1480. seconds = duration.seconds || duration.second || duration.s || 0,
  1481. milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0;
  1482. // store reference to input for deterministic cloning
  1483. this._input = duration;
  1484. // representation for dateAddRemove
  1485. this._milliseconds = +milliseconds +
  1486. seconds * 1e3 + // 1000
  1487. minutes * 6e4 + // 1000 * 60
  1488. hours * 36e5; // 1000 * 60 * 60
  1489. // Because of dateAddRemove treats 24 hours as different from a
  1490. // day when working around DST, we need to store them separately
  1491. this._days = +days +
  1492. weeks * 7;
  1493. // It is impossible translate months into days without knowing
  1494. // which months you are are talking about, so we have to store
  1495. // it separately.
  1496. this._months = +months +
  1497. years * 12;
  1498. this._data = {};
  1499. this._bubble();
  1500. }
  1501. /************************************
  1502. Helpers
  1503. ************************************/
  1504. function extend(a, b) {
  1505. for (var i in b) {
  1506. if (b.hasOwnProperty(i)) {
  1507. a[i] = b[i];
  1508. }
  1509. }
  1510. return a;
  1511. }
  1512. function absRound(number) {
  1513. if (number < 0) {
  1514. return Math.ceil(number);
  1515. } else {
  1516. return Math.floor(number);
  1517. }
  1518. }
  1519. // left zero fill a number
  1520. // see http://jsperf.com/left-zero-filling for performance comparison
  1521. function leftZeroFill(number, targetLength) {
  1522. var output = number + '';
  1523. while (output.length < targetLength) {
  1524. output = '0' + output;
  1525. }
  1526. return output;
  1527. }
  1528. // helper function for _.addTime and _.subtractTime
  1529. function addOrSubtractDurationFromMoment(mom, duration, isAdding, ignoreUpdateOffset) {
  1530. var milliseconds = duration._milliseconds,
  1531. days = duration._days,
  1532. months = duration._months,
  1533. minutes,
  1534. hours;
  1535. if (milliseconds) {
  1536. mom._d.setTime(+mom._d + milliseconds * isAdding);
  1537. }
  1538. // store the minutes and hours so we can restore them
  1539. if (days || months) {
  1540. minutes = mom.minute();
  1541. hours = mom.hour();
  1542. }
  1543. if (days) {
  1544. mom.date(mom.date() + days * isAdding);
  1545. }
  1546. if (months) {
  1547. mom.month(mom.month() + months * isAdding);
  1548. }
  1549. if (milliseconds && !ignoreUpdateOffset) {
  1550. moment.updateOffset(mom);
  1551. }
  1552. // restore the minutes and hours after possibly changing dst
  1553. if (days || months) {
  1554. mom.minute(minutes);
  1555. mom.hour(hours);
  1556. }
  1557. }
  1558. // check if is an array
  1559. function isArray(input) {
  1560. return Object.prototype.toString.call(input) === '[object Array]';
  1561. }
  1562. // compare two arrays, return the number of differences
  1563. function compareArrays(array1, array2) {
  1564. var len = Math.min(array1.length, array2.length),
  1565. lengthDiff = Math.abs(array1.length - array2.length),
  1566. diffs = 0,
  1567. i;
  1568. for (i = 0; i < len; i++) {
  1569. if (~~array1[i] !== ~~array2[i]) {
  1570. diffs++;
  1571. }
  1572. }
  1573. return diffs + lengthDiff;
  1574. }
  1575. function normalizeUnits(units) {
  1576. return units ? unitAliases[units] || units.toLowerCase().replace(/(.)s$/, '$1') : units;
  1577. }
  1578. /************************************
  1579. Languages
  1580. ************************************/
  1581. extend(Language.prototype, {
  1582. set : function (config) {
  1583. var prop, i;
  1584. for (i in config) {
  1585. prop = config[i];
  1586. if (typeof prop === 'function') {
  1587. this[i] = prop;
  1588. } else {
  1589. this['_' + i] = prop;
  1590. }
  1591. }
  1592. },
  1593. _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
  1594. months : function (m) {
  1595. return this._months[m.month()];
  1596. },
  1597. _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
  1598. monthsShort : function (m) {
  1599. return this._monthsShort[m.month()];
  1600. },
  1601. monthsParse : function (monthName) {
  1602. var i, mom, regex;
  1603. if (!this._monthsParse) {
  1604. this._monthsParse = [];
  1605. }
  1606. for (i = 0; i < 12; i++) {
  1607. // make the regex if we don't have it already
  1608. if (!this._monthsParse[i]) {
  1609. mom = moment.utc([2000, i]);
  1610. regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, '');
  1611. this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i');
  1612. }
  1613. // test the regex
  1614. if (this._monthsParse[i].test(monthName)) {
  1615. return i;
  1616. }
  1617. }
  1618. },
  1619. _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
  1620. weekdays : function (m) {
  1621. return this._weekdays[m.day()];
  1622. },
  1623. _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
  1624. weekdaysShort : function (m) {
  1625. return this._weekdaysShort[m.day()];
  1626. },
  1627. _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"),
  1628. weekdaysMin : function (m) {
  1629. return this._weekdaysMin[m.day()];
  1630. },
  1631. weekdaysParse : function (weekdayName) {
  1632. var i, mom, regex;
  1633. if (!this._weekdaysParse) {
  1634. this._weekdaysParse = [];
  1635. }
  1636. for (i = 0; i < 7; i++) {
  1637. // make the regex if we don't have it already
  1638. if (!this._weekdaysParse[i]) {
  1639. mom = moment([2000, 1]).day(i);
  1640. regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, '');
  1641. this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i');
  1642. }
  1643. // test the regex
  1644. if (this._weekdaysParse[i].test(weekdayName)) {
  1645. return i;
  1646. }
  1647. }
  1648. },
  1649. _longDateFormat : {
  1650. LT : "h:mm A",
  1651. L : "MM/DD/YYYY",
  1652. LL : "MMMM D YYYY",
  1653. LLL : "MMMM D YYYY LT",
  1654. LLLL : "dddd, MMMM D YYYY LT"
  1655. },
  1656. longDateFormat : function (key) {
  1657. var output = this._longDateFormat[key];
  1658. if (!output && this._longDateFormat[key.toUpperCase()]) {
  1659. output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) {
  1660. return val.slice(1);
  1661. });
  1662. this._longDateFormat[key] = output;
  1663. }
  1664. return output;
  1665. },
  1666. isPM : function (input) {
  1667. // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays
  1668. // Using charAt should be more compatible.
  1669. return ((input + '').toLowerCase().charAt(0) === 'p');
  1670. },
  1671. _meridiemParse : /[ap]\.?m?\.?/i,
  1672. meridiem : function (hours, minutes, isLower) {
  1673. if (hours > 11) {
  1674. return isLower ? 'pm' : 'PM';
  1675. } else {
  1676. return isLower ? 'am' : 'AM';
  1677. }
  1678. },
  1679. _calendar : {
  1680. sameDay : '[Today at] LT',
  1681. nextDay : '[Tomorrow at] LT',
  1682. nextWeek : 'dddd [at] LT',
  1683. lastDay : '[Yesterday at] LT',
  1684. lastWeek : '[Last] dddd [at] LT',
  1685. sameElse : 'L'
  1686. },
  1687. calendar : function (key, mom) {
  1688. var output = this._calendar[key];
  1689. return typeof output === 'function' ? output.apply(mom) : output;
  1690. },
  1691. _relativeTime : {
  1692. future : "in %s",
  1693. past : "%s ago",
  1694. s : "a few seconds",
  1695. m : "a minute",
  1696. mm : "%d minutes",
  1697. h : "an hour",
  1698. hh : "%d hours",
  1699. d : "a day",
  1700. dd : "%d days",
  1701. M : "a month",
  1702. MM : "%d months",
  1703. y : "a year",
  1704. yy : "%d years"
  1705. },
  1706. relativeTime : function (number, withoutSuffix, string, isFuture) {
  1707. var output = this._relativeTime[string];
  1708. return (typeof output === 'function') ?
  1709. output(number, withoutSuffix, string, isFuture) :
  1710. output.replace(/%d/i, number);
  1711. },
  1712. pastFuture : function (diff, output) {
  1713. var format = this._relativeTime[diff > 0 ? 'future' : 'past'];
  1714. return typeof format === 'function' ? format(output) : format.replace(/%s/i, output);
  1715. },
  1716. ordinal : function (number) {
  1717. return this._ordinal.replace("%d", number);
  1718. },
  1719. _ordinal : "%d",
  1720. preparse : function (string) {
  1721. return string;
  1722. },
  1723. postformat : function (string) {
  1724. return string;
  1725. },
  1726. week : function (mom) {
  1727. return weekOfYear(mom, this._week.dow, this._week.doy).week;
  1728. },
  1729. _week : {
  1730. dow : 0, // Sunday is the first day of the week.
  1731. doy : 6 // The week that contains Jan 1st is the first week of the year.
  1732. }
  1733. });
  1734. // Loads a language definition into the `languages` cache. The function
  1735. // takes a key and optionally values. If not in the browser and no values
  1736. // are provided, it will load the language file module. As a convenience,
  1737. // this function also returns the language values.
  1738. function loadLang(key, values) {
  1739. values.abbr = key;
  1740. if (!languages[key]) {
  1741. languages[key] = new Language();
  1742. }
  1743. languages[key].set(values);
  1744. return languages[key];
  1745. }
  1746. // Remove a language from the `languages` cache. Mostly useful in tests.
  1747. function unloadLang(key) {
  1748. delete languages[key];
  1749. }
  1750. // Determines which language definition to use and returns it.
  1751. //
  1752. // With no parameters, it will return the global language. If you
  1753. // pass in a language key, such as 'en', it will return the
  1754. // definition for 'en', so long as 'en' has already been loaded using
  1755. // moment.lang.
  1756. function getLangDefinition(key) {
  1757. if (!key) {
  1758. return moment.fn._lang;
  1759. }
  1760. if (!languages[key] && hasModule) {
  1761. try {
  1762. require('./lang/' + key);
  1763. } catch (e) {
  1764. // call with no params to set to default
  1765. return moment.fn._lang;
  1766. }
  1767. }
  1768. return languages[key] || moment.fn._lang;
  1769. }
  1770. /************************************
  1771. Formatting
  1772. ************************************/
  1773. function removeFormattingTokens(input) {
  1774. if (input.match(/\[.*\]/)) {
  1775. return input.replace(/^\[|\]$/g, "");
  1776. }
  1777. return input.replace(/\\/g, "");
  1778. }
  1779. function makeFormatFunction(format) {
  1780. var array = format.match(formattingTokens), i, length;
  1781. for (i = 0, length = array.length; i < length; i++) {
  1782. if (formatTokenFunctions[array[i]]) {
  1783. array[i] = formatTokenFunctions[array[i]];
  1784. } else {
  1785. array[i] = removeFormattingTokens(array[i]);
  1786. }
  1787. }
  1788. return function (mom) {
  1789. var output = "";
  1790. for (i = 0; i < length; i++) {
  1791. output += array[i] instanceof Function ? array[i].call(mom, format) : array[i];
  1792. }
  1793. return output;
  1794. };
  1795. }
  1796. // format date using native date object
  1797. function formatMoment(m, format) {
  1798. format = expandFormat(format, m.lang());
  1799. if (!formatFunctions[format]) {
  1800. formatFunctions[format] = makeFormatFunction(format);
  1801. }
  1802. return formatFunctions[format](m);
  1803. }
  1804. function expandFormat(format, lang) {
  1805. var i = 5;
  1806. function replaceLongDateFormatTokens(input) {
  1807. return lang.longDateFormat(input) || input;
  1808. }
  1809. while (i-- && (localFormattingTokens.lastIndex = 0,
  1810. localFormattingTokens.test(format))) {
  1811. format = format.replace(localFormattingTokens, replaceLongDateFormatTokens);
  1812. }
  1813. return format;
  1814. }
  1815. /************************************
  1816. Parsing
  1817. ************************************/
  1818. // get the regex to find the next token
  1819. function getParseRegexForToken(token, config) {
  1820. switch (token) {
  1821. case 'DDDD':
  1822. return parseTokenThreeDigits;
  1823. case 'YYYY':
  1824. return parseTokenFourDigits;
  1825. case 'YYYYY':
  1826. return parseTokenSixDigits;
  1827. case 'S':
  1828. case 'SS':
  1829. case 'SSS':
  1830. case 'DDD':
  1831. return parseTokenOneToThreeDigits;
  1832. case 'MMM':
  1833. case 'MMMM':
  1834. case 'dd':
  1835. case 'ddd':
  1836. case 'dddd':
  1837. return parseTokenWord;
  1838. case 'a':
  1839. case 'A':
  1840. return getLangDefinition(config._l)._meridiemParse;
  1841. case 'X':
  1842. return parseTokenTimestampMs;
  1843. case 'Z':
  1844. case 'ZZ':
  1845. return parseTokenTimezone;
  1846. case 'T':
  1847. return parseTokenT;
  1848. case 'MM':
  1849. case 'DD':
  1850. case 'YY':
  1851. case 'HH':
  1852. case 'hh':
  1853. case 'mm':
  1854. case 'ss':
  1855. case 'M':
  1856. case 'D':
  1857. case 'd':
  1858. case 'H':
  1859. case 'h':
  1860. case 'm':
  1861. case 's':
  1862. return parseTokenOneOrTwoDigits;
  1863. default :
  1864. return new RegExp(token.replace('\\', ''));
  1865. }
  1866. }
  1867. function timezoneMinutesFromString(string) {
  1868. var tzchunk = (parseTokenTimezone.exec(string) || [])[0],
  1869. parts = (tzchunk + '').match(parseTimezoneChunker) || ['-', 0, 0],
  1870. minutes = +(parts[1] * 60) + ~~parts[2];
  1871. return parts[0] === '+' ? -minutes : minutes;
  1872. }
  1873. // function to convert string input to date
  1874. function addTimeToArrayFromToken(token, input, config) {
  1875. var a, datePartArray = config._a;
  1876. switch (token) {
  1877. // MONTH
  1878. case 'M' : // fall through to MM
  1879. case 'MM' :
  1880. if (input != null) {
  1881. datePartArray[1] = ~~input - 1;
  1882. }
  1883. break;
  1884. case 'MMM' : // fall through to MMMM
  1885. case 'MMMM' :
  1886. a = getLangDefinition(config._l).monthsParse(input);
  1887. // if we didn't find a month name, mark the date as invalid.
  1888. if (a != null) {
  1889. datePartArray[1] = a;
  1890. } else {
  1891. config._isValid = false;
  1892. }
  1893. break;
  1894. // DAY OF MONTH
  1895. case 'D' : // fall through to DD
  1896. case 'DD' :
  1897. if (input != null) {
  1898. datePartArray[2] = ~~input;
  1899. }
  1900. break;
  1901. // DAY OF YEAR
  1902. case 'DDD' : // fall through to DDDD
  1903. case 'DDDD' :
  1904. if (input != null) {
  1905. datePartArray[1] = 0;
  1906. datePartArray[2] = ~~input;
  1907. }
  1908. break;
  1909. // YEAR
  1910. case 'YY' :
  1911. datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000);
  1912. break;
  1913. case 'YYYY' :
  1914. case 'YYYYY' :
  1915. datePartArray[0] = ~~input;
  1916. break;
  1917. // AM / PM
  1918. case 'a' : // fall through to A
  1919. case 'A' :
  1920. config._isPm = getLangDefinition(config._l).isPM(input);
  1921. break;
  1922. // 24 HOUR
  1923. case 'H' : // fall through to hh
  1924. case 'HH' : // fall through to hh
  1925. case 'h' : // fall through to hh
  1926. case 'hh' :
  1927. datePartArray[3] = ~~input;
  1928. break;
  1929. // MINUTE
  1930. case 'm' : // fall through to mm
  1931. case 'mm' :
  1932. datePartArray[4] = ~~input;
  1933. break;
  1934. // SECOND
  1935. case 's' : // fall through to ss
  1936. case 'ss' :
  1937. datePartArray[5] = ~~input;
  1938. break;
  1939. // MILLISECOND
  1940. case 'S' :
  1941. case 'SS' :
  1942. case 'SSS' :
  1943. datePartArray[6] = ~~ (('0.' + input) * 1000);
  1944. break;
  1945. // UNIX TIMESTAMP WITH MS
  1946. case 'X':
  1947. config._d = new Date(parseFloat(input) * 1000);
  1948. break;
  1949. // TIMEZONE
  1950. case 'Z' : // fall through to ZZ
  1951. case 'ZZ' :
  1952. config._useUTC = true;
  1953. config._tzm = timezoneMinutesFromString(input);
  1954. break;
  1955. }
  1956. // if the input is null, the date is not valid
  1957. if (input == null) {
  1958. config._isValid = false;
  1959. }
  1960. }
  1961. // convert an array to a date.
  1962. // the array should mirror the parameters below
  1963. // note: all values past the year are optional and will default to the lowest possible value.
  1964. // [year, month, day , hour, minute, second, millisecond]
  1965. function dateFromArray(config) {
  1966. var i, date, input = [], currentDate;
  1967. if (config._d) {
  1968. return;
  1969. }
  1970. // Default to current date.
  1971. // * if no year, month, day of month are given, default to today
  1972. // * if day of month is given, default month and year
  1973. // * if month is given, default only year
  1974. // * if year is given, don't default anything
  1975. currentDate = currentDateArray(config);
  1976. for (i = 0; i < 3 && config._a[i] == null; ++i) {
  1977. config._a[i] = input[i] = currentDate[i];
  1978. }
  1979. // Zero out whatever was not defaulted, including time
  1980. for (; i < 7; i++) {
  1981. config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i];
  1982. }
  1983. // add the offsets to the time to be parsed so that we can have a clean array for checking isValid
  1984. input[3] += ~~((config._tzm || 0) / 60);
  1985. input[4] += ~~((config._tzm || 0) % 60);
  1986. date = new Date(0);
  1987. if (config._useUTC) {
  1988. date.setUTCFullYear(input[0], input[1], input[2]);
  1989. date.setUTCHours(input[3], input[4], input[5], input[6]);
  1990. } else {
  1991. date.setFullYear(input[0], input[1], input[2]);
  1992. date.setHours(input[3], input[4], input[5], input[6]);
  1993. }
  1994. config._d = date;
  1995. }
  1996. function dateFromObject(config) {
  1997. var o = config._i;
  1998. if (config._d) {
  1999. return;
  2000. }
  2001. config._a = [
  2002. o.years || o.year || o.y,
  2003. o.months || o.month || o.M,
  2004. o.days || o.day || o.d,
  2005. o.hours || o.hour || o.h,
  2006. o.minutes || o.minute || o.m,
  2007. o.seconds || o.second || o.s,
  2008. o.milliseconds || o.millisecond || o.ms
  2009. ];
  2010. dateFromArray(config);
  2011. }
  2012. function currentDateArray(config) {
  2013. var now = new Date();
  2014. if (config._useUTC) {
  2015. return [
  2016. now.getUTCFullYear(),
  2017. now.getUTCMonth(),
  2018. now.getUTCDate()
  2019. ];
  2020. } else {
  2021. return [now.getFullYear(), now.getMonth(), now.getDate()];
  2022. }
  2023. }
  2024. // date from string and format string
  2025. function makeDateFromStringAndFormat(config) {
  2026. // This array is used to make a Date, either with `new Date` or `Date.UTC`
  2027. var lang = getLangDefinition(config._l),
  2028. string = '' + config._i,
  2029. i, parsedInput, tokens;
  2030. tokens = expandFormat(config._f, lang).match(formattingTokens);
  2031. config._a = [];
  2032. for (i = 0; i < tokens.length; i++) {
  2033. parsedInput = (getParseRegexForToken(tokens[i], config).exec(string) || [])[0];
  2034. if (parsedInput) {
  2035. string = string.slice(string.indexOf(parsedInput) + parsedInput.length);
  2036. }
  2037. // don't parse if its not a known token
  2038. if (formatTokenFunctions[tokens[i]]) {
  2039. addTimeToArrayFromToken(tokens[i], parsedInput, config);
  2040. }
  2041. }
  2042. // add remaining unparsed input to the string
  2043. if (string) {
  2044. config._il = string;
  2045. }
  2046. // handle am pm
  2047. if (config._isPm && config._a[3] < 12) {
  2048. config._a[3] += 12;
  2049. }
  2050. // if is 12 am, change hours to 0
  2051. if (config._isPm === false && config._a[3] === 12) {
  2052. config._a[3] = 0;
  2053. }
  2054. // return
  2055. dateFromArray(config);
  2056. }
  2057. // date from string and array of format strings
  2058. function makeDateFromStringAndArray(config) {
  2059. var tempConfig,
  2060. tempMoment,
  2061. bestMoment,
  2062. scoreToBeat = 99,
  2063. i,
  2064. currentScore;
  2065. for (i = 0; i < config._f.length; i++) {
  2066. tempConfig = extend({}, config);
  2067. tempConfig._f = config._f[i];
  2068. makeDateFromStringAndFormat(tempConfig);
  2069. tempMoment = new Moment(tempConfig);
  2070. currentScore = compareArrays(tempConfig._a, tempMoment.toArray());
  2071. // if there is any input that was not parsed
  2072. // add a penalty for that format
  2073. if (tempMoment._il) {
  2074. currentScore += tempMoment._il.length;
  2075. }
  2076. if (currentScore < scoreToBeat) {
  2077. scoreToBeat = currentScore;
  2078. bestMoment = tempMoment;
  2079. }
  2080. }
  2081. extend(config, bestMoment);
  2082. }
  2083. // date from iso format
  2084. function makeDateFromString(config) {
  2085. var i,
  2086. string = config._i,
  2087. match = isoRegex.exec(string);
  2088. if (match) {
  2089. // match[2] should be "T" or undefined
  2090. config._f = 'YYYY-MM-DD' + (match[2] || " ");
  2091. for (i = 0; i < 4; i++) {
  2092. if (isoTimes[i][1].exec(string)) {
  2093. config._f += isoTimes[i][0];
  2094. break;
  2095. }
  2096. }
  2097. if (parseTokenTimezone.exec(string)) {
  2098. config._f += " Z";
  2099. }
  2100. makeDateFromStringAndFormat(config);
  2101. } else {
  2102. config._d = new Date(string);
  2103. }
  2104. }
  2105. function makeDateFromInput(config) {
  2106. var input = config._i,
  2107. matched = aspNetJsonRegex.exec(input);
  2108. if (input === undefined) {
  2109. config._d = new Date();
  2110. } else if (matched) {
  2111. config._d = new Date(+matched[1]);
  2112. } else if (typeof input === 'string') {
  2113. makeDateFromString(config);
  2114. } else if (isArray(input)) {
  2115. config._a = input.slice(0);
  2116. dateFromArray(config);
  2117. } else if (input instanceof Date) {
  2118. config._d = new Date(+input);
  2119. } else if (typeof(input) === 'object') {
  2120. dateFromObject(config);
  2121. } else {
  2122. config._d = new Date(input);
  2123. }
  2124. }
  2125. /************************************
  2126. Relative Time
  2127. ************************************/
  2128. // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize
  2129. function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) {
  2130. return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture);
  2131. }
  2132. function relativeTime(milliseconds, withoutSuffix, lang) {
  2133. var seconds = round(Math.abs(milliseconds) / 1000),
  2134. minutes = round(seconds / 60),
  2135. hours = round(minutes / 60),
  2136. days = round(hours / 24),
  2137. years = round(days / 365),
  2138. args = seconds < 45 && ['s', seconds] ||
  2139. minutes === 1 && ['m'] ||
  2140. minutes < 45 && ['mm', minutes] ||
  2141. hours === 1 && ['h'] ||
  2142. hours < 22 && ['hh', hours] ||
  2143. days === 1 && ['d'] ||
  2144. days <= 25 && ['dd', days] ||
  2145. days <= 45 && ['M'] ||
  2146. days < 345 && ['MM', round(days / 30)] ||
  2147. years === 1 && ['y'] || ['yy', years];
  2148. args[2] = withoutSuffix;
  2149. args[3] = milliseconds > 0;
  2150. args[4] = lang;
  2151. return substituteTimeAgo.apply({}, args);
  2152. }
  2153. /************************************
  2154. Week of Year
  2155. ************************************/
  2156. // firstDayOfWeek 0 = sun, 6 = sat
  2157. // the day of the week that starts the week
  2158. // (usually sunday or monday)
  2159. // firstDayOfWeekOfYear 0 = sun, 6 = sat
  2160. // the first week is the week that contains the first
  2161. // of this day of the week
  2162. // (eg. ISO weeks use thursday (4))
  2163. function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) {
  2164. var end = firstDayOfWeekOfYear - firstDayOfWeek,
  2165. daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(),
  2166. adjustedMoment;
  2167. if (daysToDayOfWeek > end) {
  2168. daysToDayOfWeek -= 7;
  2169. }
  2170. if (daysToDayOfWeek < end - 7) {
  2171. daysToDayOfWeek += 7;
  2172. }
  2173. adjustedMoment = moment(mom).add('d', daysToDayOfWeek);
  2174. return {
  2175. week: Math.ceil(adjustedMoment.dayOfYear() / 7),
  2176. year: adjustedMoment.year()
  2177. };
  2178. }
  2179. /************************************
  2180. Top Level Functions
  2181. ************************************/
  2182. function makeMoment(config) {
  2183. var input = config._i,
  2184. format = config._f;
  2185. if (input === null || input === '') {
  2186. return null;
  2187. }
  2188. if (typeof input === 'string') {
  2189. config._i = input = getLangDefinition().preparse(input);
  2190. }
  2191. if (moment.isMoment(input)) {
  2192. config = extend({}, input);
  2193. config._d = new Date(+input._d);
  2194. } else if (format) {
  2195. if (isArray(format)) {
  2196. makeDateFromStringAndArray(config);
  2197. } else {
  2198. makeDateFromStringAndFormat(config);
  2199. }
  2200. } else {
  2201. makeDateFromInput(config);
  2202. }
  2203. return new Moment(config);
  2204. }
  2205. moment = function (input, format, lang) {
  2206. return makeMoment({
  2207. _i : input,
  2208. _f : format,
  2209. _l : lang,
  2210. _isUTC : false
  2211. });
  2212. };
  2213. // creating with utc
  2214. moment.utc = function (input, format, lang) {
  2215. return makeMoment({
  2216. _useUTC : true,
  2217. _isUTC : true,
  2218. _l : lang,
  2219. _i : input,
  2220. _f : format
  2221. }).utc();
  2222. };
  2223. // creating with unix timestamp (in seconds)
  2224. moment.unix = function (input) {
  2225. return moment(input * 1000);
  2226. };
  2227. // duration
  2228. moment.duration = function (input, key) {
  2229. var isDuration = moment.isDuration(input),
  2230. isNumber = (typeof input === 'number'),
  2231. duration = (isDuration ? input._input : (isNumber ? {} : input)),
  2232. matched = aspNetTimeSpanJsonRegex.exec(input),
  2233. sign,
  2234. ret;
  2235. if (isNumber) {
  2236. if (key) {
  2237. duration[key] = input;
  2238. } else {
  2239. duration.milliseconds = input;
  2240. }
  2241. } else if (matched) {
  2242. sign = (matched[1] === "-") ? -1 : 1;
  2243. duration = {
  2244. y: 0,
  2245. d: ~~matched[2] * sign,
  2246. h: ~~matched[3] * sign,
  2247. m: ~~matched[4] * sign,
  2248. s: ~~matched[5] * sign,
  2249. ms: ~~matched[6] * sign
  2250. };
  2251. }
  2252. ret = new Duration(duration);
  2253. if (isDuration && input.hasOwnProperty('_lang')) {
  2254. ret._lang = input._lang;
  2255. }
  2256. return ret;
  2257. };
  2258. // version number
  2259. moment.version = VERSION;
  2260. // default format
  2261. moment.defaultFormat = isoFormat;
  2262. // This function will be called whenever a moment is mutated.
  2263. // It is intended to keep the offset in sync with the timezone.
  2264. moment.updateOffset = function () {};
  2265. // This function will load languages and then set the global language. If
  2266. // no arguments are passed in, it will simply return the current global
  2267. // language key.
  2268. moment.lang = function (key, values) {
  2269. if (!key) {
  2270. return moment.fn._lang._abbr;
  2271. }
  2272. key = key.toLowerCase();
  2273. key = key.replace('_', '-');
  2274. if (values) {
  2275. loadLang(key, values);
  2276. } else if (values === null) {
  2277. unloadLang(key);
  2278. key = 'en';
  2279. } else if (!languages[key]) {
  2280. getLangDefinition(key);
  2281. }
  2282. moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key);
  2283. };
  2284. // returns language data
  2285. moment.langData = function (key) {
  2286. if (key && key._lang && key._lang._abbr) {
  2287. key = key._lang._abbr;
  2288. }
  2289. return getLangDefinition(key);
  2290. };
  2291. // compare moment object
  2292. moment.isMoment = function (obj) {
  2293. return obj instanceof Moment;
  2294. };
  2295. // for typechecking Duration objects
  2296. moment.isDuration = function (obj) {
  2297. return obj instanceof Duration;
  2298. };
  2299. /************************************
  2300. Moment Prototype
  2301. ************************************/
  2302. extend(moment.fn = Moment.prototype, {
  2303. clone : function () {
  2304. return moment(this);
  2305. },
  2306. valueOf : function () {
  2307. return +this._d + ((this._offset || 0) * 60000);
  2308. },
  2309. unix : function () {
  2310. return Math.floor(+this / 1000);
  2311. },
  2312. toString : function () {
  2313. return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ");
  2314. },
  2315. toDate : function () {
  2316. return this._offset ? new Date(+this) : this._d;
  2317. },
  2318. toISOString : function () {
  2319. return formatMoment(moment(this).utc(), 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]');
  2320. },
  2321. toArray : function () {
  2322. var m = this;
  2323. return [
  2324. m.year(),
  2325. m.month(),
  2326. m.date(),
  2327. m.hours(),
  2328. m.minutes(),
  2329. m.seconds(),
  2330. m.milliseconds()
  2331. ];
  2332. },
  2333. isValid : function () {
  2334. if (this._isValid == null) {
  2335. if (this._a) {
  2336. this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray());
  2337. } else {
  2338. this._isValid = !isNaN(this._d.getTime());
  2339. }
  2340. }
  2341. return !!this._isValid;
  2342. },
  2343. invalidAt: function () {
  2344. var i, arr1 = this._a, arr2 = (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray();
  2345. for (i = 6; i >= 0 && arr1[i] === arr2[i]; --i) {
  2346. // empty loop body
  2347. }
  2348. return i;
  2349. },
  2350. utc : function () {
  2351. return this.zone(0);
  2352. },
  2353. local : function () {
  2354. this.zone(0);
  2355. this._isUTC = false;
  2356. return this;
  2357. },
  2358. format : function (inputString) {
  2359. var output = formatMoment(this, inputString || moment.defaultFormat);
  2360. return this.lang().postformat(output);
  2361. },
  2362. add : function (input, val) {
  2363. var dur;
  2364. // switch args to support add('s', 1) and add(1, 's')
  2365. if (typeof input === 'string') {
  2366. dur = moment.duration(+val, input);
  2367. } else {
  2368. dur = moment.duration(input, val);
  2369. }
  2370. addOrSubtractDurationFromMoment(this, dur, 1);
  2371. return this;
  2372. },
  2373. subtract : function (input, val) {
  2374. var dur;
  2375. // switch args to support subtract('s', 1) and subtract(1, 's')
  2376. if (typeof input === 'string') {
  2377. dur = moment.duration(+val, input);
  2378. } else {
  2379. dur = moment.duration(input, val);
  2380. }
  2381. addOrSubtractDurationFromMoment(this, dur, -1);
  2382. return this;
  2383. },
  2384. diff : function (input, units, asFloat) {
  2385. var that = this._isUTC ? moment(input).zone(this._offset || 0) : moment(input).local(),
  2386. zoneDiff = (this.zone() - that.zone()) * 6e4,
  2387. diff, output;
  2388. units = normalizeUnits(units);
  2389. if (units === 'year' || units === 'month') {
  2390. // average number of days in the months in the given dates
  2391. diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2
  2392. // difference in months
  2393. output = ((this.year() - that.year()) * 12) + (this.month() - that.month());
  2394. // adjust by taking difference in days, average number of days
  2395. // and dst in the given months.
  2396. output += ((this - moment(this).startOf('month')) -
  2397. (that - moment(that).startOf('month'))) / diff;
  2398. // same as above but with zones, to negate all dst
  2399. output -= ((this.zone() - moment(this).startOf('month').zone()) -
  2400. (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff;
  2401. if (units === 'year') {
  2402. output = output / 12;
  2403. }
  2404. } else {
  2405. diff = (this - that);
  2406. output = units === 'second' ? diff / 1e3 : // 1000
  2407. units === 'minute' ? diff / 6e4 : // 1000 * 60
  2408. units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60
  2409. units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst
  2410. units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst
  2411. diff;
  2412. }
  2413. return asFloat ? output : absRound(output);
  2414. },
  2415. from : function (time, withoutSuffix) {
  2416. return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix);
  2417. },
  2418. fromNow : function (withoutSuffix) {
  2419. return this.from(moment(), withoutSuffix);
  2420. },
  2421. calendar : function () {
  2422. var diff = this.diff(moment().zone(this.zone()).startOf('day'), 'days', true),
  2423. format = diff < -6 ? 'sameElse' :
  2424. diff < -1 ? 'lastWeek' :
  2425. diff < 0 ? 'lastDay' :
  2426. diff < 1 ? 'sameDay' :
  2427. diff < 2 ? 'nextDay' :
  2428. diff < 7 ? 'nextWeek' : 'sameElse';
  2429. return this.format(this.lang().calendar(format, this));
  2430. },
  2431. isLeapYear : function () {
  2432. var year = this.year();
  2433. return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
  2434. },
  2435. isDST : function () {
  2436. return (this.zone() < this.clone().month(0).zone() ||
  2437. this.zone() < this.clone().month(5).zone());
  2438. },
  2439. day : function (input) {
  2440. var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay();
  2441. if (input != null) {
  2442. if (typeof input === 'string') {
  2443. input = this.lang().weekdaysParse(input);
  2444. if (typeof input !== 'number') {
  2445. return this;
  2446. }
  2447. }
  2448. return this.add({ d : input - day });
  2449. } else {
  2450. return day;
  2451. }
  2452. },
  2453. month : function (input) {
  2454. var utc = this._isUTC ? 'UTC' : '',
  2455. dayOfMonth;
  2456. if (input != null) {
  2457. if (typeof input === 'string') {
  2458. input = this.lang().monthsParse(input);
  2459. if (typeof input !== 'number') {
  2460. return this;
  2461. }
  2462. }
  2463. dayOfMonth = this.date();
  2464. this.date(1);
  2465. this._d['set' + utc + 'Month'](input);
  2466. this.date(Math.min(dayOfMonth, this.daysInMonth()));
  2467. moment.updateOffset(this);
  2468. return this;
  2469. } else {
  2470. return this._d['get' + utc + 'Month']();
  2471. }
  2472. },
  2473. startOf: function (units) {
  2474. units = normalizeUnits(units);
  2475. // the following switch intentionally omits break keywords
  2476. // to utilize falling through the cases.
  2477. switch (units) {
  2478. case 'year':
  2479. this.month(0);
  2480. /* falls through */
  2481. case 'month':
  2482. this.date(1);
  2483. /* falls through */
  2484. case 'week':
  2485. case 'isoweek':
  2486. case 'day':
  2487. this.hours(0);
  2488. /* falls through */
  2489. case 'hour':
  2490. this.minutes(0);
  2491. /* falls through */
  2492. case 'minute':
  2493. this.seconds(0);
  2494. /* falls through */
  2495. case 'second':
  2496. this.milliseconds(0);
  2497. /* falls through */
  2498. }
  2499. // weeks are a special case
  2500. if (units === 'week') {
  2501. this.weekday(0);
  2502. } else if (units === 'isoweek') {
  2503. this.isoWeekday(1);
  2504. }
  2505. return this;
  2506. },
  2507. endOf: function (units) {
  2508. units = normalizeUnits(units);
  2509. return this.startOf(units).add((units === 'isoweek' ? 'week' : units), 1).subtract('ms', 1);
  2510. },
  2511. isAfter: function (input, units) {
  2512. units = typeof units !== 'undefined' ? units : 'millisecond';
  2513. return +this.clone().startOf(units) > +moment(input).startOf(units);
  2514. },
  2515. isBefore: function (input, units) {
  2516. units = typeof units !== 'undefined' ? units : 'millisecond';
  2517. return +this.clone().startOf(units) < +moment(input).startOf(units);
  2518. },
  2519. isSame: function (input, units) {
  2520. units = typeof units !== 'undefined' ? units : 'millisecond';
  2521. return +this.clone().startOf(units) === +moment(input).startOf(units);
  2522. },
  2523. min: function (other) {
  2524. other = moment.apply(null, arguments);
  2525. return other < this ? this : other;
  2526. },
  2527. max: function (other) {
  2528. other = moment.apply(null, arguments);
  2529. return other > this ? this : other;
  2530. },
  2531. zone : function (input) {
  2532. var offset = this._offset || 0;
  2533. if (input != null) {
  2534. if (typeof input === "string") {
  2535. input = timezoneMinutesFromString(input);
  2536. }
  2537. if (Math.abs(input) < 16) {
  2538. input = input * 60;
  2539. }
  2540. this._offset = input;
  2541. this._isUTC = true;
  2542. if (offset !== input) {
  2543. addOrSubtractDurationFromMoment(this, moment.duration(offset - input, 'm'), 1, true);
  2544. }
  2545. } else {
  2546. return this._isUTC ? offset : this._d.getTimezoneOffset();
  2547. }
  2548. return this;
  2549. },
  2550. zoneAbbr : function () {
  2551. return this._isUTC ? "UTC" : "";
  2552. },
  2553. zoneName : function () {
  2554. return this._isUTC ? "Coordinated Universal Time" : "";
  2555. },
  2556. hasAlignedHourOffset : function (input) {
  2557. if (!input) {
  2558. input = 0;
  2559. }
  2560. else {
  2561. input = moment(input).zone();
  2562. }
  2563. return (this.zone() - input) % 60 === 0;
  2564. },
  2565. daysInMonth : function () {
  2566. return moment.utc([this.year(), this.month() + 1, 0]).date();
  2567. },
  2568. dayOfYear : function (input) {
  2569. var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1;
  2570. return input == null ? dayOfYear : this.add("d", (input - dayOfYear));
  2571. },
  2572. weekYear : function (input) {
  2573. var year = weekOfYear(this, this.lang()._week.dow, this.lang()._week.doy).year;
  2574. return input == null ? year : this.add("y", (input - year));
  2575. },
  2576. isoWeekYear : function (input) {
  2577. var year = weekOfYear(this, 1, 4).year;
  2578. return input == null ? year : this.add("y", (input - year));
  2579. },
  2580. week : function (input) {
  2581. var week = this.lang().week(this);
  2582. return input == null ? week : this.add("d", (input - week) * 7);
  2583. },
  2584. isoWeek : function (input) {
  2585. var week = weekOfYear(this, 1, 4).week;
  2586. return input == null ? week : this.add("d", (input - week) * 7);
  2587. },
  2588. weekday : function (input) {
  2589. var weekday = (this._d.getDay() + 7 - this.lang()._week.dow) % 7;
  2590. return input == null ? weekday : this.add("d", input - weekday);
  2591. },
  2592. isoWeekday : function (input) {
  2593. // behaves the same as moment#day except
  2594. // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6)
  2595. // as a setter, sunday should belong to the previous week.
  2596. return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7);
  2597. },
  2598. get : function (units) {
  2599. units = normalizeUnits(units);
  2600. return this[units.toLowerCase()]();
  2601. },
  2602. set : function (units, value) {
  2603. units = normalizeUnits(units);
  2604. this[units.toLowerCase()](value);
  2605. },
  2606. // If passed a language key, it will set the language for this
  2607. // instance. Otherwise, it will return the language configuration
  2608. // variables for this instance.
  2609. lang : function (key) {
  2610. if (key === undefined) {
  2611. return this._lang;
  2612. } else {
  2613. this._lang = getLangDefinition(key);
  2614. return this;
  2615. }
  2616. }
  2617. });
  2618. // helper for adding shortcuts
  2619. function makeGetterAndSetter(name, key) {
  2620. moment.fn[name] = moment.fn[name + 's'] = function (input) {
  2621. var utc = this._isUTC ? 'UTC' : '';
  2622. if (input != null) {
  2623. this._d['set' + utc + key](input);
  2624. moment.updateOffset(this);
  2625. return this;
  2626. } else {
  2627. return this._d['get' + utc + key]();
  2628. }
  2629. };
  2630. }
  2631. // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
  2632. for (i = 0; i < proxyGettersAndSetters.length; i ++) {
  2633. makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]);
  2634. }
  2635. // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
  2636. makeGetterAndSetter('year', 'FullYear');
  2637. // add plural methods
  2638. moment.fn.days = moment.fn.day;
  2639. moment.fn.months = moment.fn.month;
  2640. moment.fn.weeks = moment.fn.week;
  2641. moment.fn.isoWeeks = moment.fn.isoWeek;
  2642. // add aliased format methods
  2643. moment.fn.toJSON = moment.fn.toISOString;
  2644. /************************************
  2645. Duration Prototype
  2646. ************************************/
  2647. extend(moment.duration.fn = Duration.prototype, {
  2648. _bubble : function () {
  2649. var milliseconds = this._milliseconds,
  2650. days = this._days,
  2651. months = this._months,
  2652. data = this._data,
  2653. seconds, minutes, hours, years;
  2654. // The following code bubbles up values, see the tests for
  2655. // examples of what that means.
  2656. data.milliseconds = milliseconds % 1000;
  2657. seconds = absRound(milliseconds / 1000);
  2658. data.seconds = seconds % 60;
  2659. minutes = absRound(seconds / 60);
  2660. data.minutes = minutes % 60;
  2661. hours = absRound(minutes / 60);
  2662. data.hours = hours % 24;
  2663. days += absRound(hours / 24);
  2664. data.days = days % 30;
  2665. months += absRound(days / 30);
  2666. data.months = months % 12;
  2667. years = absRound(months / 12);
  2668. data.years = years;
  2669. },
  2670. weeks : function () {
  2671. return absRound(this.days() / 7);
  2672. },
  2673. valueOf : function () {
  2674. return this._milliseconds +
  2675. this._days * 864e5 +
  2676. (this._months % 12) * 2592e6 +
  2677. ~~(this._months / 12) * 31536e6;
  2678. },
  2679. humanize : function (withSuffix) {
  2680. var difference = +this,
  2681. output = relativeTime(difference, !withSuffix, this.lang());
  2682. if (withSuffix) {
  2683. output = this.lang().pastFuture(difference, output);
  2684. }
  2685. return this.lang().postformat(output);
  2686. },
  2687. add : function (input, val) {
  2688. // supports only 2.0-style add(1, 's') or add(moment)
  2689. var dur = moment.duration(input, val);
  2690. this._milliseconds += dur._milliseconds;
  2691. this._days += dur._days;
  2692. this._months += dur._months;
  2693. this._bubble();
  2694. return this;
  2695. },
  2696. subtract : function (input, val) {
  2697. var dur = moment.duration(input, val);
  2698. this._milliseconds -= dur._milliseconds;
  2699. this._days -= dur._days;
  2700. this._months -= dur._months;
  2701. this._bubble();
  2702. return this;
  2703. },
  2704. get : function (units) {
  2705. units = normalizeUnits(units);
  2706. return this[units.toLowerCase() + 's']();
  2707. },
  2708. as : function (units) {
  2709. units = normalizeUnits(units);
  2710. return this['as' + units.charAt(0).toUpperCase() + units.slice(1) + 's']();
  2711. },
  2712. lang : moment.fn.lang
  2713. });
  2714. function makeDurationGetter(name) {
  2715. moment.duration.fn[name] = function () {
  2716. return this._data[name];
  2717. };
  2718. }
  2719. function makeDurationAsGetter(name, factor) {
  2720. moment.duration.fn['as' + name] = function () {
  2721. return +this / factor;
  2722. };
  2723. }
  2724. for (i in unitMillisecondFactors) {
  2725. if (unitMillisecondFactors.hasOwnProperty(i)) {
  2726. makeDurationAsGetter(i, unitMillisecondFactors[i]);
  2727. makeDurationGetter(i.toLowerCase());
  2728. }
  2729. }
  2730. makeDurationAsGetter('Weeks', 6048e5);
  2731. moment.duration.fn.asMonths = function () {
  2732. return (+this - this.years() * 31536e6) / 2592e6 + this.years() * 12;
  2733. };
  2734. /************************************
  2735. Default Lang
  2736. ************************************/
  2737. // Set default language, other languages will inherit from English.
  2738. moment.lang('en', {
  2739. ordinal : function (number) {
  2740. var b = number % 10,
  2741. output = (~~ (number % 100 / 10) === 1) ? 'th' :
  2742. (b === 1) ? 'st' :
  2743. (b === 2) ? 'nd' :
  2744. (b === 3) ? 'rd' : 'th';
  2745. return number + output;
  2746. }
  2747. });
  2748. /* EMBED_LANGUAGES */
  2749. /************************************
  2750. Exposing Moment
  2751. ************************************/
  2752. // CommonJS module is defined
  2753. if (hasModule) {
  2754. module.exports = moment;
  2755. }
  2756. /*global ender:false */
  2757. if (typeof ender === 'undefined') {
  2758. // here, `this` means `window` in the browser, or `global` on the server
  2759. // add `moment` as a global object via a string identifier,
  2760. // for Closure Compiler "advanced" mode
  2761. this['moment'] = moment;
  2762. }
  2763. /*global define:false */
  2764. if (typeof define === "function" && define.amd) {
  2765. define("moment", [], function () {
  2766. return moment;
  2767. });
  2768. }
  2769. }).call(this);
  2770. },{}],3:[function(require,module,exports){
  2771. /**
  2772. * vis.js module imports
  2773. */
  2774. // Try to load dependencies from the global window object.
  2775. // If not available there, load via require.
  2776. var moment = (typeof window !== 'undefined') && window['moment'] || require('moment');
  2777. var Hammer = (typeof window !== 'undefined') && window['Hammer'] || require('hammerjs');
  2778. // Internet Explorer 8 and older does not support Array.indexOf, so we define
  2779. // it here in that case.
  2780. // http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
  2781. if(!Array.prototype.indexOf) {
  2782. Array.prototype.indexOf = function(obj){
  2783. for(var i = 0; i < this.length; i++){
  2784. if(this[i] == obj){
  2785. return i;
  2786. }
  2787. }
  2788. return -1;
  2789. };
  2790. try {
  2791. console.log("Warning: Ancient browser detected. Please update your browser");
  2792. }
  2793. catch (err) {
  2794. }
  2795. }
  2796. // Internet Explorer 8 and older does not support Array.forEach, so we define
  2797. // it here in that case.
  2798. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach
  2799. if (!Array.prototype.forEach) {
  2800. Array.prototype.forEach = function(fn, scope) {
  2801. for(var i = 0, len = this.length; i < len; ++i) {
  2802. fn.call(scope || this, this[i], i, this);
  2803. }
  2804. }
  2805. }
  2806. // Internet Explorer 8 and older does not support Array.map, so we define it
  2807. // here in that case.
  2808. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/map
  2809. // Production steps of ECMA-262, Edition 5, 15.4.4.19
  2810. // Reference: http://es5.github.com/#x15.4.4.19
  2811. if (!Array.prototype.map) {
  2812. Array.prototype.map = function(callback, thisArg) {
  2813. var T, A, k;
  2814. if (this == null) {
  2815. throw new TypeError(" this is null or not defined");
  2816. }
  2817. // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
  2818. var O = Object(this);
  2819. // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
  2820. // 3. Let len be ToUint32(lenValue).
  2821. var len = O.length >>> 0;
  2822. // 4. If IsCallable(callback) is false, throw a TypeError exception.
  2823. // See: http://es5.github.com/#x9.11
  2824. if (typeof callback !== "function") {
  2825. throw new TypeError(callback + " is not a function");
  2826. }
  2827. // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
  2828. if (thisArg) {
  2829. T = thisArg;
  2830. }
  2831. // 6. Let A be a new array created as if by the expression new Array(len) where Array is
  2832. // the standard built-in constructor with that name and len is the value of len.
  2833. A = new Array(len);
  2834. // 7. Let k be 0
  2835. k = 0;
  2836. // 8. Repeat, while k < len
  2837. while(k < len) {
  2838. var kValue, mappedValue;
  2839. // a. Let Pk be ToString(k).
  2840. // This is implicit for LHS operands of the in operator
  2841. // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
  2842. // This step can be combined with c
  2843. // c. If kPresent is true, then
  2844. if (k in O) {
  2845. // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
  2846. kValue = O[ k ];
  2847. // ii. Let mappedValue be the result of calling the Call internal method of callback
  2848. // with T as the this value and argument list containing kValue, k, and O.
  2849. mappedValue = callback.call(T, kValue, k, O);
  2850. // iii. Call the DefineOwnProperty internal method of A with arguments
  2851. // Pk, Property Descriptor {Value: mappedValue, : true, Enumerable: true, Configurable: true},
  2852. // and false.
  2853. // In browsers that support Object.defineProperty, use the following:
  2854. // Object.defineProperty(A, Pk, { value: mappedValue, writable: true, enumerable: true, configurable: true });
  2855. // For best browser support, use the following:
  2856. A[ k ] = mappedValue;
  2857. }
  2858. // d. Increase k by 1.
  2859. k++;
  2860. }
  2861. // 9. return A
  2862. return A;
  2863. };
  2864. }
  2865. // Internet Explorer 8 and older does not support Array.filter, so we define it
  2866. // here in that case.
  2867. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/filter
  2868. if (!Array.prototype.filter) {
  2869. Array.prototype.filter = function(fun /*, thisp */) {
  2870. "use strict";
  2871. if (this == null) {
  2872. throw new TypeError();
  2873. }
  2874. var t = Object(this);
  2875. var len = t.length >>> 0;
  2876. if (typeof fun != "function") {
  2877. throw new TypeError();
  2878. }
  2879. var res = [];
  2880. var thisp = arguments[1];
  2881. for (var i = 0; i < len; i++) {
  2882. if (i in t) {
  2883. var val = t[i]; // in case fun mutates this
  2884. if (fun.call(thisp, val, i, t))
  2885. res.push(val);
  2886. }
  2887. }
  2888. return res;
  2889. };
  2890. }
  2891. // Internet Explorer 8 and older does not support Object.keys, so we define it
  2892. // here in that case.
  2893. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys
  2894. if (!Object.keys) {
  2895. Object.keys = (function () {
  2896. var hasOwnProperty = Object.prototype.hasOwnProperty,
  2897. hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
  2898. dontEnums = [
  2899. 'toString',
  2900. 'toLocaleString',
  2901. 'valueOf',
  2902. 'hasOwnProperty',
  2903. 'isPrototypeOf',
  2904. 'propertyIsEnumerable',
  2905. 'constructor'
  2906. ],
  2907. dontEnumsLength = dontEnums.length;
  2908. return function (obj) {
  2909. if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) {
  2910. throw new TypeError('Object.keys called on non-object');
  2911. }
  2912. var result = [];
  2913. for (var prop in obj) {
  2914. if (hasOwnProperty.call(obj, prop)) result.push(prop);
  2915. }
  2916. if (hasDontEnumBug) {
  2917. for (var i=0; i < dontEnumsLength; i++) {
  2918. if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
  2919. }
  2920. }
  2921. return result;
  2922. }
  2923. })()
  2924. }
  2925. // Internet Explorer 8 and older does not support Array.isArray,
  2926. // so we define it here in that case.
  2927. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/isArray
  2928. if(!Array.isArray) {
  2929. Array.isArray = function (vArg) {
  2930. return Object.prototype.toString.call(vArg) === "[object Array]";
  2931. };
  2932. }
  2933. // Internet Explorer 8 and older does not support Function.bind,
  2934. // so we define it here in that case.
  2935. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
  2936. if (!Function.prototype.bind) {
  2937. Function.prototype.bind = function (oThis) {
  2938. if (typeof this !== "function") {
  2939. // closest thing possible to the ECMAScript 5 internal IsCallable function
  2940. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  2941. }
  2942. var aArgs = Array.prototype.slice.call(arguments, 1),
  2943. fToBind = this,
  2944. fNOP = function () {},
  2945. fBound = function () {
  2946. return fToBind.apply(this instanceof fNOP && oThis
  2947. ? this
  2948. : oThis,
  2949. aArgs.concat(Array.prototype.slice.call(arguments)));
  2950. };
  2951. fNOP.prototype = this.prototype;
  2952. fBound.prototype = new fNOP();
  2953. return fBound;
  2954. };
  2955. }
  2956. // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
  2957. if (!Object.create) {
  2958. Object.create = function (o) {
  2959. if (arguments.length > 1) {
  2960. throw new Error('Object.create implementation only accepts the first parameter.');
  2961. }
  2962. function F() {}
  2963. F.prototype = o;
  2964. return new F();
  2965. };
  2966. }
  2967. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
  2968. if (!Function.prototype.bind) {
  2969. Function.prototype.bind = function (oThis) {
  2970. if (typeof this !== "function") {
  2971. // closest thing possible to the ECMAScript 5 internal IsCallable function
  2972. throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
  2973. }
  2974. var aArgs = Array.prototype.slice.call(arguments, 1),
  2975. fToBind = this,
  2976. fNOP = function () {},
  2977. fBound = function () {
  2978. return fToBind.apply(this instanceof fNOP && oThis
  2979. ? this
  2980. : oThis,
  2981. aArgs.concat(Array.prototype.slice.call(arguments)));
  2982. };
  2983. fNOP.prototype = this.prototype;
  2984. fBound.prototype = new fNOP();
  2985. return fBound;
  2986. };
  2987. }
  2988. /**
  2989. * utility functions
  2990. */
  2991. var util = {};
  2992. /**
  2993. * Test whether given object is a number
  2994. * @param {*} object
  2995. * @return {Boolean} isNumber
  2996. */
  2997. util.isNumber = function isNumber(object) {
  2998. return (object instanceof Number || typeof object == 'number');
  2999. };
  3000. /**
  3001. * Test whether given object is a string
  3002. * @param {*} object
  3003. * @return {Boolean} isString
  3004. */
  3005. util.isString = function isString(object) {
  3006. return (object instanceof String || typeof object == 'string');
  3007. };
  3008. /**
  3009. * Test whether given object is a Date, or a String containing a Date
  3010. * @param {Date | String} object
  3011. * @return {Boolean} isDate
  3012. */
  3013. util.isDate = function isDate(object) {
  3014. if (object instanceof Date) {
  3015. return true;
  3016. }
  3017. else if (util.isString(object)) {
  3018. // test whether this string contains a date
  3019. var match = ASPDateRegex.exec(object);
  3020. if (match) {
  3021. return true;
  3022. }
  3023. else if (!isNaN(Date.parse(object))) {
  3024. return true;
  3025. }
  3026. }
  3027. return false;
  3028. };
  3029. /**
  3030. * Test whether given object is an instance of google.visualization.DataTable
  3031. * @param {*} object
  3032. * @return {Boolean} isDataTable
  3033. */
  3034. util.isDataTable = function isDataTable(object) {
  3035. return (typeof (google) !== 'undefined') &&
  3036. (google.visualization) &&
  3037. (google.visualization.DataTable) &&
  3038. (object instanceof google.visualization.DataTable);
  3039. };
  3040. /**
  3041. * Create a semi UUID
  3042. * source: http://stackoverflow.com/a/105074/1262753
  3043. * @return {String} uuid
  3044. */
  3045. util.randomUUID = function randomUUID () {
  3046. var S4 = function () {
  3047. return Math.floor(
  3048. Math.random() * 0x10000 /* 65536 */
  3049. ).toString(16);
  3050. };
  3051. return (
  3052. S4() + S4() + '-' +
  3053. S4() + '-' +
  3054. S4() + '-' +
  3055. S4() + '-' +
  3056. S4() + S4() + S4()
  3057. );
  3058. };
  3059. /**
  3060. * Extend object a with the properties of object b or a series of objects
  3061. * Only properties with defined values are copied
  3062. * @param {Object} a
  3063. * @param {... Object} b
  3064. * @return {Object} a
  3065. */
  3066. util.extend = function (a, b) {
  3067. for (var i = 1, len = arguments.length; i < len; i++) {
  3068. var other = arguments[i];
  3069. for (var prop in other) {
  3070. if (other.hasOwnProperty(prop) && other[prop] !== undefined) {
  3071. a[prop] = other[prop];
  3072. }
  3073. }
  3074. }
  3075. return a;
  3076. };
  3077. /**
  3078. * Convert an object to another type
  3079. * @param {Boolean | Number | String | Date | Moment | Null | undefined} object
  3080. * @param {String | undefined} type Name of the type. Available types:
  3081. * 'Boolean', 'Number', 'String',
  3082. * 'Date', 'Moment', ISODate', 'ASPDate'.
  3083. * @return {*} object
  3084. * @throws Error
  3085. */
  3086. util.convert = function convert(object, type) {
  3087. var match;
  3088. if (object === undefined) {
  3089. return undefined;
  3090. }
  3091. if (object === null) {
  3092. return null;
  3093. }
  3094. if (!type) {
  3095. return object;
  3096. }
  3097. if (!(typeof type === 'string') && !(type instanceof String)) {
  3098. throw new Error('Type must be a string');
  3099. }
  3100. //noinspection FallthroughInSwitchStatementJS
  3101. switch (type) {
  3102. case 'boolean':
  3103. case 'Boolean':
  3104. return Boolean(object);
  3105. case 'number':
  3106. case 'Number':
  3107. return Number(object.valueOf());
  3108. case 'string':
  3109. case 'String':
  3110. return String(object);
  3111. case 'Date':
  3112. if (util.isNumber(object)) {
  3113. return new Date(object);
  3114. }
  3115. if (object instanceof Date) {
  3116. return new Date(object.valueOf());
  3117. }
  3118. else if (moment.isMoment(object)) {
  3119. return new Date(object.valueOf());
  3120. }
  3121. if (util.isString(object)) {
  3122. match = ASPDateRegex.exec(object);
  3123. if (match) {
  3124. // object is an ASP date
  3125. return new Date(Number(match[1])); // parse number
  3126. }
  3127. else {
  3128. return moment(object).toDate(); // parse string
  3129. }
  3130. }
  3131. else {
  3132. throw new Error(
  3133. 'Cannot convert object of type ' + util.getType(object) +
  3134. ' to type Date');
  3135. }
  3136. case 'Moment':
  3137. if (util.isNumber(object)) {
  3138. return moment(object);
  3139. }
  3140. if (object instanceof Date) {
  3141. return moment(object.valueOf());
  3142. }
  3143. else if (moment.isMoment(object)) {
  3144. return moment.clone();
  3145. }
  3146. if (util.isString(object)) {
  3147. match = ASPDateRegex.exec(object);
  3148. if (match) {
  3149. // object is an ASP date
  3150. return moment(Number(match[1])); // parse number
  3151. }
  3152. else {
  3153. return moment(object); // parse string
  3154. }
  3155. }
  3156. else {
  3157. throw new Error(
  3158. 'Cannot convert object of type ' + util.getType(object) +
  3159. ' to type Date');
  3160. }
  3161. case 'ISODate':
  3162. if (util.isNumber(object)) {
  3163. return new Date(object);
  3164. }
  3165. else if (object instanceof Date) {
  3166. return object.toISOString();
  3167. }
  3168. else if (moment.isMoment(object)) {
  3169. return object.toDate().toISOString();
  3170. }
  3171. else if (util.isString(object)) {
  3172. match = ASPDateRegex.exec(object);
  3173. if (match) {
  3174. // object is an ASP date
  3175. return new Date(Number(match[1])).toISOString(); // parse number
  3176. }
  3177. else {
  3178. return new Date(object).toISOString(); // parse string
  3179. }
  3180. }
  3181. else {
  3182. throw new Error(
  3183. 'Cannot convert object of type ' + util.getType(object) +
  3184. ' to type ISODate');
  3185. }
  3186. case 'ASPDate':
  3187. if (util.isNumber(object)) {
  3188. return '/Date(' + object + ')/';
  3189. }
  3190. else if (object instanceof Date) {
  3191. return '/Date(' + object.valueOf() + ')/';
  3192. }
  3193. else if (util.isString(object)) {
  3194. match = ASPDateRegex.exec(object);
  3195. var value;
  3196. if (match) {
  3197. // object is an ASP date
  3198. value = new Date(Number(match[1])).valueOf(); // parse number
  3199. }
  3200. else {
  3201. value = new Date(object).valueOf(); // parse string
  3202. }
  3203. return '/Date(' + value + ')/';
  3204. }
  3205. else {
  3206. throw new Error(
  3207. 'Cannot convert object of type ' + util.getType(object) +
  3208. ' to type ASPDate');
  3209. }
  3210. default:
  3211. throw new Error('Cannot convert object of type ' + util.getType(object) +
  3212. ' to type "' + type + '"');
  3213. }
  3214. };
  3215. // parse ASP.Net Date pattern,
  3216. // for example '/Date(1198908717056)/' or '/Date(1198908717056-0700)/'
  3217. // code from http://momentjs.com/
  3218. var ASPDateRegex = /^\/?Date\((\-?\d+)/i;
  3219. /**
  3220. * Get the type of an object, for example util.getType([]) returns 'Array'
  3221. * @param {*} object
  3222. * @return {String} type
  3223. */
  3224. util.getType = function getType(object) {
  3225. var type = typeof object;
  3226. if (type == 'object') {
  3227. if (object == null) {
  3228. return 'null';
  3229. }
  3230. if (object instanceof Boolean) {
  3231. return 'Boolean';
  3232. }
  3233. if (object instanceof Number) {
  3234. return 'Number';
  3235. }
  3236. if (object instanceof String) {
  3237. return 'String';
  3238. }
  3239. if (object instanceof Array) {
  3240. return 'Array';
  3241. }
  3242. if (object instanceof Date) {
  3243. return 'Date';
  3244. }
  3245. return 'Object';
  3246. }
  3247. else if (type == 'number') {
  3248. return 'Number';
  3249. }
  3250. else if (type == 'boolean') {
  3251. return 'Boolean';
  3252. }
  3253. else if (type == 'string') {
  3254. return 'String';
  3255. }
  3256. return type;
  3257. };
  3258. /**
  3259. * Retrieve the absolute left value of a DOM element
  3260. * @param {Element} elem A dom element, for example a div
  3261. * @return {number} left The absolute left position of this element
  3262. * in the browser page.
  3263. */
  3264. util.getAbsoluteLeft = function getAbsoluteLeft (elem) {
  3265. var doc = document.documentElement;
  3266. var body = document.body;
  3267. var left = elem.offsetLeft;
  3268. var e = elem.offsetParent;
  3269. while (e != null && e != body && e != doc) {
  3270. left += e.offsetLeft;
  3271. left -= e.scrollLeft;
  3272. e = e.offsetParent;
  3273. }
  3274. return left;
  3275. };
  3276. /**
  3277. * Retrieve the absolute top value of a DOM element
  3278. * @param {Element} elem A dom element, for example a div
  3279. * @return {number} top The absolute top position of this element
  3280. * in the browser page.
  3281. */
  3282. util.getAbsoluteTop = function getAbsoluteTop (elem) {
  3283. var doc = document.documentElement;
  3284. var body = document.body;
  3285. var top = elem.offsetTop;
  3286. var e = elem.offsetParent;
  3287. while (e != null && e != body && e != doc) {
  3288. top += e.offsetTop;
  3289. top -= e.scrollTop;
  3290. e = e.offsetParent;
  3291. }
  3292. return top;
  3293. };
  3294. /**
  3295. * Get the absolute, vertical mouse position from an event.
  3296. * @param {Event} event
  3297. * @return {Number} pageY
  3298. */
  3299. util.getPageY = function getPageY (event) {
  3300. if ('pageY' in event) {
  3301. return event.pageY;
  3302. }
  3303. else {
  3304. var clientY;
  3305. if (('targetTouches' in event) && event.targetTouches.length) {
  3306. clientY = event.targetTouches[0].clientY;
  3307. }
  3308. else {
  3309. clientY = event.clientY;
  3310. }
  3311. var doc = document.documentElement;
  3312. var body = document.body;
  3313. return clientY +
  3314. ( doc && doc.scrollTop || body && body.scrollTop || 0 ) -
  3315. ( doc && doc.clientTop || body && body.clientTop || 0 );
  3316. }
  3317. };
  3318. /**
  3319. * Get the absolute, horizontal mouse position from an event.
  3320. * @param {Event} event
  3321. * @return {Number} pageX
  3322. */
  3323. util.getPageX = function getPageX (event) {
  3324. if ('pageY' in event) {
  3325. return event.pageX;
  3326. }
  3327. else {
  3328. var clientX;
  3329. if (('targetTouches' in event) && event.targetTouches.length) {
  3330. clientX = event.targetTouches[0].clientX;
  3331. }
  3332. else {
  3333. clientX = event.clientX;
  3334. }
  3335. var doc = document.documentElement;
  3336. var body = document.body;
  3337. return clientX +
  3338. ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) -
  3339. ( doc && doc.clientLeft || body && body.clientLeft || 0 );
  3340. }
  3341. };
  3342. /**
  3343. * add a className to the given elements style
  3344. * @param {Element} elem
  3345. * @param {String} className
  3346. */
  3347. util.addClassName = function addClassName(elem, className) {
  3348. var classes = elem.className.split(' ');
  3349. if (classes.indexOf(className) == -1) {
  3350. classes.push(className); // add the class to the array
  3351. elem.className = classes.join(' ');
  3352. }
  3353. };
  3354. /**
  3355. * add a className to the given elements style
  3356. * @param {Element} elem
  3357. * @param {String} className
  3358. */
  3359. util.removeClassName = function removeClassname(elem, className) {
  3360. var classes = elem.className.split(' ');
  3361. var index = classes.indexOf(className);
  3362. if (index != -1) {
  3363. classes.splice(index, 1); // remove the class from the array
  3364. elem.className = classes.join(' ');
  3365. }
  3366. };
  3367. /**
  3368. * For each method for both arrays and objects.
  3369. * In case of an array, the built-in Array.forEach() is applied.
  3370. * In case of an Object, the method loops over all properties of the object.
  3371. * @param {Object | Array} object An Object or Array
  3372. * @param {function} callback Callback method, called for each item in
  3373. * the object or array with three parameters:
  3374. * callback(value, index, object)
  3375. */
  3376. <<<<<<< HEAD
  3377. Stack.prototype.collision = function collision (a, b, margin) {
  3378. var a_width;
  3379. var b_width;
  3380. if (a.props.content !== undefined && a.width < a.props.content.width)
  3381. a_width = a.props.content.width;
  3382. else
  3383. a_width = a.width;
  3384. if (b.props.content !== undefined && b.width < b.props.content.width)
  3385. b_width = b.props.content.width;
  3386. else
  3387. b_width = b.width
  3388. return ((a.left - margin) < (b.left + b_width) &&
  3389. (a.left + a_width + margin) > b.left &&
  3390. (a.top - margin) < (b.top + b.height) &&
  3391. (a.top + a.height + margin) > b.top);
  3392. =======
  3393. util.forEach = function forEach (object, callback) {
  3394. var i,
  3395. len;
  3396. if (object instanceof Array) {
  3397. // array
  3398. for (i = 0, len = object.length; i < len; i++) {
  3399. callback(object[i], i, object);
  3400. }
  3401. }
  3402. else {
  3403. // object
  3404. for (i in object) {
  3405. if (object.hasOwnProperty(i)) {
  3406. callback(object[i], i, object);
  3407. }
  3408. }
  3409. }
  3410. >>>>>>> upstream/develop
  3411. };
  3412. /**
  3413. * Update a property in an object
  3414. * @param {Object} object
  3415. * @param {String} key
  3416. * @param {*} value
  3417. * @return {Boolean} changed
  3418. */
  3419. util.updateProperty = function updateProp (object, key, value) {
  3420. if (object[key] !== value) {
  3421. object[key] = value;
  3422. return true;
  3423. }
  3424. else {
  3425. return false;
  3426. }
  3427. };
  3428. /**
  3429. * Add and event listener. Works for all browsers
  3430. * @param {Element} element An html element
  3431. * @param {string} action The action, for example "click",
  3432. * without the prefix "on"
  3433. * @param {function} listener The callback function to be executed
  3434. * @param {boolean} [useCapture]
  3435. */
  3436. util.addEventListener = function addEventListener(element, action, listener, useCapture) {
  3437. if (element.addEventListener) {
  3438. if (useCapture === undefined)
  3439. useCapture = false;
  3440. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  3441. action = "DOMMouseScroll"; // For Firefox
  3442. }
  3443. element.addEventListener(action, listener, useCapture);
  3444. } else {
  3445. element.attachEvent("on" + action, listener); // IE browsers
  3446. }
  3447. };
  3448. /**
  3449. * Remove an event listener from an element
  3450. * @param {Element} element An html dom element
  3451. * @param {string} action The name of the event, for example "mousedown"
  3452. * @param {function} listener The listener function
  3453. * @param {boolean} [useCapture]
  3454. */
  3455. util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
  3456. if (element.removeEventListener) {
  3457. // non-IE browsers
  3458. if (useCapture === undefined)
  3459. useCapture = false;
  3460. if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) {
  3461. action = "DOMMouseScroll"; // For Firefox
  3462. }
  3463. element.removeEventListener(action, listener, useCapture);
  3464. } else {
  3465. // IE browsers
  3466. element.detachEvent("on" + action, listener);
  3467. }
  3468. };
  3469. /**
  3470. * Get HTML element which is the target of the event
  3471. * @param {Event} event
  3472. * @return {Element} target element
  3473. */
  3474. util.getTarget = function getTarget(event) {
  3475. // code from http://www.quirksmode.org/js/events_properties.html
  3476. if (!event) {
  3477. event = window.event;
  3478. }
  3479. var target;
  3480. if (event.target) {
  3481. target = event.target;
  3482. }
  3483. else if (event.srcElement) {
  3484. target = event.srcElement;
  3485. }
  3486. if (target.nodeType != undefined && target.nodeType == 3) {
  3487. // defeat Safari bug
  3488. target = target.parentNode;
  3489. }
  3490. return target;
  3491. };
  3492. /**
  3493. * Stop event propagation
  3494. */
  3495. util.stopPropagation = function stopPropagation(event) {
  3496. if (!event)
  3497. event = window.event;
  3498. if (event.stopPropagation) {
  3499. event.stopPropagation(); // non-IE browsers
  3500. }
  3501. else {
  3502. event.cancelBubble = true; // IE browsers
  3503. }
  3504. };
  3505. /**
  3506. * Cancels the event if it is cancelable, without stopping further propagation of the event.
  3507. */
  3508. util.preventDefault = function preventDefault (event) {
  3509. if (!event)
  3510. event = window.event;
  3511. if (event.preventDefault) {
  3512. event.preventDefault(); // non-IE browsers
  3513. }
  3514. else {
  3515. event.returnValue = false; // IE browsers
  3516. }
  3517. };
  3518. util.option = {};
  3519. /**
  3520. * Convert a value into a boolean
  3521. * @param {Boolean | function | undefined} value
  3522. * @param {Boolean} [defaultValue]
  3523. * @returns {Boolean} bool
  3524. */
  3525. util.option.asBoolean = function (value, defaultValue) {
  3526. if (typeof value == 'function') {
  3527. value = value();
  3528. }
  3529. if (value != null) {
  3530. return (value != false);
  3531. }
  3532. return defaultValue || null;
  3533. };
  3534. /**
  3535. * Convert a value into a number
  3536. * @param {Boolean | function | undefined} value
  3537. * @param {Number} [defaultValue]
  3538. * @returns {Number} number
  3539. */
  3540. util.option.asNumber = function (value, defaultValue) {
  3541. if (typeof value == 'function') {
  3542. value = value();
  3543. }
  3544. if (value != null) {
  3545. return Number(value) || defaultValue || null;
  3546. }
  3547. return defaultValue || null;
  3548. };
  3549. /**
  3550. * Convert a value into a string
  3551. * @param {String | function | undefined} value
  3552. * @param {String} [defaultValue]
  3553. * @returns {String} str
  3554. */
  3555. util.option.asString = function (value, defaultValue) {
  3556. if (typeof value == 'function') {
  3557. value = value();
  3558. }
  3559. if (value != null) {
  3560. return String(value);
  3561. }
  3562. return defaultValue || null;
  3563. };
  3564. /**
  3565. * Convert a size or location into a string with pixels or a percentage
  3566. * @param {String | Number | function | undefined} value
  3567. * @param {String} [defaultValue]
  3568. * @returns {String} size
  3569. */
  3570. util.option.asSize = function (value, defaultValue) {
  3571. if (typeof value == 'function') {
  3572. value = value();
  3573. }
  3574. if (util.isString(value)) {
  3575. return value;
  3576. }
  3577. else if (util.isNumber(value)) {
  3578. return value + 'px';
  3579. }
  3580. else {
  3581. return defaultValue || null;
  3582. }
  3583. };
  3584. /**
  3585. * Convert a value into a DOM element
  3586. * @param {HTMLElement | function | undefined} value
  3587. * @param {HTMLElement} [defaultValue]
  3588. * @returns {HTMLElement | null} dom
  3589. */
  3590. util.option.asElement = function (value, defaultValue) {
  3591. if (typeof value == 'function') {
  3592. value = value();
  3593. }
  3594. return value || defaultValue || null;
  3595. };
  3596. /**
  3597. * load css from text
  3598. * @param {String} css Text containing css
  3599. */
  3600. util.loadCss = function (css) {
  3601. if (typeof document === 'undefined') {
  3602. return;
  3603. }
  3604. // get the script location, and built the css file name from the js file name
  3605. // http://stackoverflow.com/a/2161748/1262753
  3606. // var scripts = document.getElementsByTagName('script');
  3607. // var jsFile = scripts[scripts.length-1].src.split('?')[0];
  3608. // var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
  3609. // inject css
  3610. // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
  3611. var style = document.createElement('style');
  3612. style.type = 'text/css';
  3613. if (style.styleSheet){
  3614. style.styleSheet.cssText = css;
  3615. } else {
  3616. style.appendChild(document.createTextNode(css));
  3617. }
  3618. document.getElementsByTagName('head')[0].appendChild(style);
  3619. };
  3620. /**
  3621. * Event listener (singleton)
  3622. */
  3623. // TODO: replace usage of the event listener for the EventBus
  3624. var events = {
  3625. 'listeners': [],
  3626. /**
  3627. * Find a single listener by its object
  3628. * @param {Object} object
  3629. * @return {Number} index -1 when not found
  3630. */
  3631. 'indexOf': function (object) {
  3632. var listeners = this.listeners;
  3633. for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
  3634. var listener = listeners[i];
  3635. if (listener && listener.object == object) {
  3636. return i;
  3637. }
  3638. }
  3639. return -1;
  3640. },
  3641. /**
  3642. * Add an event listener
  3643. * @param {Object} object
  3644. * @param {String} event The name of an event, for example 'select'
  3645. * @param {function} callback The callback method, called when the
  3646. * event takes place
  3647. */
  3648. 'addListener': function (object, event, callback) {
  3649. var index = this.indexOf(object);
  3650. var listener = this.listeners[index];
  3651. if (!listener) {
  3652. listener = {
  3653. 'object': object,
  3654. 'events': {}
  3655. };
  3656. this.listeners.push(listener);
  3657. }
  3658. var callbacks = listener.events[event];
  3659. if (!callbacks) {
  3660. callbacks = [];
  3661. listener.events[event] = callbacks;
  3662. }
  3663. // add the callback if it does not yet exist
  3664. if (callbacks.indexOf(callback) == -1) {
  3665. callbacks.push(callback);
  3666. }
  3667. },
  3668. /**
  3669. * Remove an event listener
  3670. * @param {Object} object
  3671. * @param {String} event The name of an event, for example 'select'
  3672. * @param {function} callback The registered callback method
  3673. */
  3674. 'removeListener': function (object, event, callback) {
  3675. var index = this.indexOf(object);
  3676. var listener = this.listeners[index];
  3677. if (listener) {
  3678. var callbacks = listener.events[event];
  3679. if (callbacks) {
  3680. index = callbacks.indexOf(callback);
  3681. if (index != -1) {
  3682. callbacks.splice(index, 1);
  3683. }
  3684. // remove the array when empty
  3685. if (callbacks.length == 0) {
  3686. delete listener.events[event];
  3687. }
  3688. }
  3689. // count the number of registered events. remove listener when empty
  3690. var count = 0;
  3691. var events = listener.events;
  3692. for (var e in events) {
  3693. if (events.hasOwnProperty(e)) {
  3694. count++;
  3695. }
  3696. }
  3697. if (count == 0) {
  3698. delete this.listeners[index];
  3699. }
  3700. }
  3701. },
  3702. /**
  3703. * Remove all registered event listeners
  3704. */
  3705. 'removeAllListeners': function () {
  3706. this.listeners = [];
  3707. },
  3708. /**
  3709. * Trigger an event. All registered event handlers will be called
  3710. * @param {Object} object
  3711. * @param {String} event
  3712. * @param {Object} properties (optional)
  3713. */
  3714. 'trigger': function (object, event, properties) {
  3715. var index = this.indexOf(object);
  3716. var listener = this.listeners[index];
  3717. if (listener) {
  3718. var callbacks = listener.events[event];
  3719. if (callbacks) {
  3720. for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
  3721. callbacks[i](properties);
  3722. }
  3723. }
  3724. }
  3725. }
  3726. };
  3727. /**
  3728. * An event bus can be used to emit events, and to subscribe to events
  3729. * @constructor EventBus
  3730. */
  3731. function EventBus() {
  3732. this.subscriptions = [];
  3733. }
  3734. /**
  3735. * Subscribe to an event
  3736. * @param {String | RegExp} event The event can be a regular expression, or
  3737. * a string with wildcards, like 'server.*'.
  3738. * @param {function} callback. Callback are called with three parameters:
  3739. * {String} event, {*} [data], {*} [source]
  3740. * @param {*} [target]
  3741. * @returns {String} id A subscription id
  3742. */
  3743. EventBus.prototype.on = function (event, callback, target) {
  3744. var regexp = (event instanceof RegExp) ?
  3745. event :
  3746. new RegExp(event.replace('*', '\\w+'));
  3747. var subscription = {
  3748. id: util.randomUUID(),
  3749. event: event,
  3750. regexp: regexp,
  3751. callback: (typeof callback === 'function') ? callback : null,
  3752. target: target
  3753. };
  3754. this.subscriptions.push(subscription);
  3755. return subscription.id;
  3756. };
  3757. /**
  3758. * Unsubscribe from an event
  3759. * @param {String | Object} filter Filter for subscriptions to be removed
  3760. * Filter can be a string containing a
  3761. * subscription id, or an object containing
  3762. * one or more of the fields id, event,
  3763. * callback, and target.
  3764. */
  3765. EventBus.prototype.off = function (filter) {
  3766. var i = 0;
  3767. while (i < this.subscriptions.length) {
  3768. var subscription = this.subscriptions[i];
  3769. var match = true;
  3770. if (filter instanceof Object) {
  3771. // filter is an object. All fields must match
  3772. for (var prop in filter) {
  3773. if (filter.hasOwnProperty(prop)) {
  3774. if (filter[prop] !== subscription[prop]) {
  3775. match = false;
  3776. }
  3777. }
  3778. }
  3779. }
  3780. else {
  3781. // filter is a string, filter on id
  3782. match = (subscription.id == filter);
  3783. }
  3784. if (match) {
  3785. this.subscriptions.splice(i, 1);
  3786. }
  3787. else {
  3788. i++;
  3789. }
  3790. }
  3791. };
  3792. /**
  3793. * Emit an event
  3794. * @param {String} event
  3795. * @param {*} [data]
  3796. * @param {*} [source]
  3797. */
  3798. EventBus.prototype.emit = function (event, data, source) {
  3799. for (var i =0; i < this.subscriptions.length; i++) {
  3800. var subscription = this.subscriptions[i];
  3801. if (subscription.regexp.test(event)) {
  3802. if (subscription.callback) {
  3803. subscription.callback(event, data, source);
  3804. }
  3805. }
  3806. }
  3807. };
  3808. /**
  3809. * DataSet
  3810. *
  3811. * Usage:
  3812. * var dataSet = new DataSet({
  3813. * fieldId: '_id',
  3814. * convert: {
  3815. * // ...
  3816. * }
  3817. * });
  3818. *
  3819. * dataSet.add(item);
  3820. * dataSet.add(data);
  3821. * dataSet.update(item);
  3822. * dataSet.update(data);
  3823. * dataSet.remove(id);
  3824. * dataSet.remove(ids);
  3825. * var data = dataSet.get();
  3826. * var data = dataSet.get(id);
  3827. * var data = dataSet.get(ids);
  3828. * var data = dataSet.get(ids, options, data);
  3829. * dataSet.clear();
  3830. *
  3831. * A data set can:
  3832. * - add/remove/update data
  3833. * - gives triggers upon changes in the data
  3834. * - can import/export data in various data formats
  3835. *
  3836. * @param {Object} [options] Available options:
  3837. * {String} fieldId Field name of the id in the
  3838. * items, 'id' by default.
  3839. * {Object.<String, String} convert
  3840. * A map with field names as key,
  3841. * and the field type as value.
  3842. * @constructor DataSet
  3843. */
  3844. // TODO: add a DataSet constructor DataSet(data, options)
  3845. function DataSet (options) {
  3846. this.id = util.randomUUID();
  3847. this.options = options || {};
  3848. this.data = {}; // map with data indexed by id
  3849. this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
  3850. this.convert = {}; // field types by field name
  3851. if (this.options.convert) {
  3852. for (var field in this.options.convert) {
  3853. if (this.options.convert.hasOwnProperty(field)) {
  3854. var value = this.options.convert[field];
  3855. if (value == 'Date' || value == 'ISODate' || value == 'ASPDate') {
  3856. this.convert[field] = 'Date';
  3857. }
  3858. else {
  3859. this.convert[field] = value;
  3860. }
  3861. }
  3862. }
  3863. }
  3864. // event subscribers
  3865. this.subscribers = {};
  3866. this.internalIds = {}; // internally generated id's
  3867. }
  3868. /**
  3869. * Subscribe to an event, add an event listener
  3870. * @param {String} event Event name. Available events: 'put', 'update',
  3871. * 'remove'
  3872. * @param {function} callback Callback method. Called with three parameters:
  3873. * {String} event
  3874. * {Object | null} params
  3875. * {String | Number} senderId
  3876. */
  3877. DataSet.prototype.subscribe = function (event, callback) {
  3878. var subscribers = this.subscribers[event];
  3879. if (!subscribers) {
  3880. subscribers = [];
  3881. this.subscribers[event] = subscribers;
  3882. }
  3883. subscribers.push({
  3884. callback: callback
  3885. });
  3886. };
  3887. /**
  3888. * Unsubscribe from an event, remove an event listener
  3889. * @param {String} event
  3890. * @param {function} callback
  3891. */
  3892. DataSet.prototype.unsubscribe = function (event, callback) {
  3893. var subscribers = this.subscribers[event];
  3894. if (subscribers) {
  3895. this.subscribers[event] = subscribers.filter(function (listener) {
  3896. return (listener.callback != callback);
  3897. });
  3898. }
  3899. };
  3900. /**
  3901. * Trigger an event
  3902. * @param {String} event
  3903. * @param {Object | null} params
  3904. * @param {String} [senderId] Optional id of the sender.
  3905. * @private
  3906. */
  3907. DataSet.prototype._trigger = function (event, params, senderId) {
  3908. if (event == '*') {
  3909. throw new Error('Cannot trigger event *');
  3910. }
  3911. var subscribers = [];
  3912. if (event in this.subscribers) {
  3913. subscribers = subscribers.concat(this.subscribers[event]);
  3914. }
  3915. if ('*' in this.subscribers) {
  3916. subscribers = subscribers.concat(this.subscribers['*']);
  3917. }
  3918. for (var i = 0; i < subscribers.length; i++) {
  3919. var subscriber = subscribers[i];
  3920. if (subscriber.callback) {
  3921. subscriber.callback(event, params, senderId || null);
  3922. }
  3923. }
  3924. };
  3925. /**
  3926. * Add data.
  3927. * Adding an item will fail when there already is an item with the same id.
  3928. * @param {Object | Array | DataTable} data
  3929. * @param {String} [senderId] Optional sender id
  3930. * @return {Array} addedIds Array with the ids of the added items
  3931. */
  3932. DataSet.prototype.add = function (data, senderId) {
  3933. var addedIds = [],
  3934. id,
  3935. me = this;
  3936. if (data instanceof Array) {
  3937. // Array
  3938. for (var i = 0, len = data.length; i < len; i++) {
  3939. id = me._addItem(data[i]);
  3940. addedIds.push(id);
  3941. }
  3942. }
  3943. else if (util.isDataTable(data)) {
  3944. // Google DataTable
  3945. var columns = this._getColumnNames(data);
  3946. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  3947. var item = {};
  3948. for (var col = 0, cols = columns.length; col < cols; col++) {
  3949. var field = columns[col];
  3950. item[field] = data.getValue(row, col);
  3951. }
  3952. id = me._addItem(item);
  3953. addedIds.push(id);
  3954. }
  3955. }
  3956. else if (data instanceof Object) {
  3957. // Single item
  3958. id = me._addItem(data);
  3959. addedIds.push(id);
  3960. }
  3961. else {
  3962. throw new Error('Unknown dataType');
  3963. }
  3964. if (addedIds.length) {
  3965. this._trigger('add', {items: addedIds}, senderId);
  3966. }
  3967. return addedIds;
  3968. };
  3969. /**
  3970. * Update existing items. When an item does not exist, it will be created
  3971. * @param {Object | Array | DataTable} data
  3972. * @param {String} [senderId] Optional sender id
  3973. * @return {Array} updatedIds The ids of the added or updated items
  3974. */
  3975. DataSet.prototype.update = function (data, senderId) {
  3976. var addedIds = [],
  3977. updatedIds = [],
  3978. me = this,
  3979. fieldId = me.fieldId;
  3980. var addOrUpdate = function (item) {
  3981. var id = item[fieldId];
  3982. if (me.data[id]) {
  3983. // update item
  3984. id = me._updateItem(item);
  3985. updatedIds.push(id);
  3986. }
  3987. else {
  3988. // add new item
  3989. id = me._addItem(item);
  3990. addedIds.push(id);
  3991. }
  3992. };
  3993. if (data instanceof Array) {
  3994. // Array
  3995. for (var i = 0, len = data.length; i < len; i++) {
  3996. addOrUpdate(data[i]);
  3997. }
  3998. }
  3999. else if (util.isDataTable(data)) {
  4000. // Google DataTable
  4001. var columns = this._getColumnNames(data);
  4002. for (var row = 0, rows = data.getNumberOfRows(); row < rows; row++) {
  4003. var item = {};
  4004. for (var col = 0, cols = columns.length; col < cols; col++) {
  4005. var field = columns[col];
  4006. item[field] = data.getValue(row, col);
  4007. }
  4008. addOrUpdate(item);
  4009. }
  4010. }
  4011. else if (data instanceof Object) {
  4012. // Single item
  4013. addOrUpdate(data);
  4014. }
  4015. else {
  4016. throw new Error('Unknown dataType');
  4017. }
  4018. if (addedIds.length) {
  4019. this._trigger('add', {items: addedIds}, senderId);
  4020. }
  4021. if (updatedIds.length) {
  4022. this._trigger('update', {items: updatedIds}, senderId);
  4023. }
  4024. return addedIds.concat(updatedIds);
  4025. };
  4026. /**
  4027. <<<<<<< HEAD
  4028. * Move the range to a new center point
  4029. * @param {Number} moveTo New center point of the range
  4030. */
  4031. Range.prototype.moveTo = function(moveTo) {
  4032. var center = (this.start + this.end) / 2;
  4033. var diff = center - moveTo;
  4034. // calculate new start and end
  4035. var newStart = this.start - diff;
  4036. var newEnd = this.end - diff;
  4037. this.setRange(newStart, newEnd);
  4038. }
  4039. /**
  4040. * @constructor Controller
  4041. =======
  4042. * Get a data item or multiple items.
  4043. >>>>>>> upstream/develop
  4044. *
  4045. * Usage:
  4046. *
  4047. * get()
  4048. * get(options: Object)
  4049. * get(options: Object, data: Array | DataTable)
  4050. *
  4051. * get(id: Number | String)
  4052. * get(id: Number | String, options: Object)
  4053. * get(id: Number | String, options: Object, data: Array | DataTable)
  4054. *
  4055. * get(ids: Number[] | String[])
  4056. * get(ids: Number[] | String[], options: Object)
  4057. * get(ids: Number[] | String[], options: Object, data: Array | DataTable)
  4058. *
  4059. * Where:
  4060. *
  4061. * {Number | String} id The id of an item
  4062. * {Number[] | String{}} ids An array with ids of items
  4063. * {Object} options An Object with options. Available options:
  4064. * {String} [type] Type of data to be returned. Can
  4065. * be 'DataTable' or 'Array' (default)
  4066. * {Object.<String, String>} [convert]
  4067. * {String[]} [fields] field names to be returned
  4068. * {function} [filter] filter items
  4069. * {String | function} [order] Order the items by
  4070. * a field name or custom sort function.
  4071. * {Array | DataTable} [data] If provided, items will be appended to this
  4072. * array or table. Required in case of Google
  4073. * DataTable.
  4074. *
  4075. * @throws Error
  4076. */
  4077. DataSet.prototype.get = function (args) {
  4078. var me = this;
  4079. // parse the arguments
  4080. var id, ids, options, data;
  4081. var firstType = util.getType(arguments[0]);
  4082. if (firstType == 'String' || firstType == 'Number') {
  4083. // get(id [, options] [, data])
  4084. id = arguments[0];
  4085. options = arguments[1];
  4086. data = arguments[2];
  4087. }
  4088. else if (firstType == 'Array') {
  4089. // get(ids [, options] [, data])
  4090. ids = arguments[0];
  4091. options = arguments[1];
  4092. data = arguments[2];
  4093. }
  4094. else {
  4095. // get([, options] [, data])
  4096. options = arguments[0];
  4097. data = arguments[1];
  4098. }
  4099. // determine the return type
  4100. var type;
  4101. if (options && options.type) {
  4102. type = (options.type == 'DataTable') ? 'DataTable' : 'Array';
  4103. if (data && (type != util.getType(data))) {
  4104. throw new Error('Type of parameter "data" (' + util.getType(data) + ') ' +
  4105. 'does not correspond with specified options.type (' + options.type + ')');
  4106. }
  4107. if (type == 'DataTable' && !util.isDataTable(data)) {
  4108. throw new Error('Parameter "data" must be a DataTable ' +
  4109. 'when options.type is "DataTable"');
  4110. }
  4111. }
  4112. else if (data) {
  4113. type = (util.getType(data) == 'DataTable') ? 'DataTable' : 'Array';
  4114. }
  4115. else {
  4116. type = 'Array';
  4117. }
  4118. // build options
  4119. var convert = options && options.convert || this.options.convert;
  4120. var filter = options && options.filter;
  4121. var items = [], item, itemId, i, len;
  4122. // convert items
  4123. if (id != undefined) {
  4124. // return a single item
  4125. item = me._getItem(id, convert);
  4126. if (filter && !filter(item)) {
  4127. item = null;
  4128. }
  4129. }
  4130. else if (ids != undefined) {
  4131. // return a subset of items
  4132. for (i = 0, len = ids.length; i < len; i++) {
  4133. item = me._getItem(ids[i], convert);
  4134. if (!filter || filter(item)) {
  4135. items.push(item);
  4136. }
  4137. }
  4138. }
  4139. else {
  4140. // return all items
  4141. for (itemId in this.data) {
  4142. if (this.data.hasOwnProperty(itemId)) {
  4143. item = me._getItem(itemId, convert);
  4144. if (!filter || filter(item)) {
  4145. items.push(item);
  4146. }
  4147. }
  4148. }
  4149. }
  4150. // order the results
  4151. if (options && options.order && id == undefined) {
  4152. this._sort(items, options.order);
  4153. }
  4154. // filter fields of the items
  4155. if (options && options.fields) {
  4156. var fields = options.fields;
  4157. if (id != undefined) {
  4158. item = this._filterFields(item, fields);
  4159. }
  4160. else {
  4161. for (i = 0, len = items.length; i < len; i++) {
  4162. items[i] = this._filterFields(items[i], fields);
  4163. }
  4164. }
  4165. }
  4166. // return the results
  4167. if (type == 'DataTable') {
  4168. var columns = this._getColumnNames(data);
  4169. if (id != undefined) {
  4170. // append a single item to the data table
  4171. me._appendRow(data, columns, item);
  4172. }
  4173. else {
  4174. // copy the items to the provided data table
  4175. for (i = 0, len = items.length; i < len; i++) {
  4176. me._appendRow(data, columns, items[i]);
  4177. }
  4178. }
  4179. return data;
  4180. }
  4181. else {
  4182. // return an array
  4183. if (id != undefined) {
  4184. // a single item
  4185. return item;
  4186. }
  4187. else {
  4188. // multiple items
  4189. if (data) {
  4190. // copy the items to the provided array
  4191. for (i = 0, len = items.length; i < len; i++) {
  4192. data.push(items[i]);
  4193. }
  4194. return data;
  4195. }
  4196. else {
  4197. // just return our array
  4198. return items;
  4199. }
  4200. }
  4201. }
  4202. };
  4203. /**
  4204. * Get ids of all items or from a filtered set of items.
  4205. * @param {Object} [options] An Object with options. Available options:
  4206. * {function} [filter] filter items
  4207. * {String | function} [order] Order the items by
  4208. * a field name or custom sort function.
  4209. * @return {Array} ids
  4210. */
  4211. DataSet.prototype.getIds = function (options) {
  4212. var data = this.data,
  4213. filter = options && options.filter,
  4214. order = options && options.order,
  4215. convert = options && options.convert || this.options.convert,
  4216. i,
  4217. len,
  4218. id,
  4219. item,
  4220. items,
  4221. ids = [];
  4222. if (filter) {
  4223. // get filtered items
  4224. if (order) {
  4225. // create ordered list
  4226. items = [];
  4227. for (id in data) {
  4228. if (data.hasOwnProperty(id)) {
  4229. item = this._getItem(id, convert);
  4230. if (filter(item)) {
  4231. items.push(item);
  4232. }
  4233. }
  4234. }
  4235. this._sort(items, order);
  4236. for (i = 0, len = items.length; i < len; i++) {
  4237. ids[i] = items[i][this.fieldId];
  4238. }
  4239. }
  4240. else {
  4241. // create unordered list
  4242. for (id in data) {
  4243. if (data.hasOwnProperty(id)) {
  4244. item = this._getItem(id, convert);
  4245. if (filter(item)) {
  4246. ids.push(item[this.fieldId]);
  4247. }
  4248. }
  4249. }
  4250. }
  4251. }
  4252. else {
  4253. // get all items
  4254. if (order) {
  4255. // create an ordered list
  4256. items = [];
  4257. for (id in data) {
  4258. if (data.hasOwnProperty(id)) {
  4259. items.push(data[id]);
  4260. }
  4261. }
  4262. this._sort(items, order);
  4263. for (i = 0, len = items.length; i < len; i++) {
  4264. ids[i] = items[i][this.fieldId];
  4265. }
  4266. }
  4267. else {
  4268. // create unordered list
  4269. for (id in data) {
  4270. if (data.hasOwnProperty(id)) {
  4271. item = data[id];
  4272. ids.push(item[this.fieldId]);
  4273. }
  4274. }
  4275. }
  4276. }
  4277. return ids;
  4278. };
  4279. /**
  4280. * Execute a callback function for every item in the dataset.
  4281. * The order of the items is not determined.
  4282. * @param {function} callback
  4283. * @param {Object} [options] Available options:
  4284. * {Object.<String, String>} [convert]
  4285. * {String[]} [fields] filter fields
  4286. * {function} [filter] filter items
  4287. * {String | function} [order] Order the items by
  4288. * a field name or custom sort function.
  4289. */
  4290. DataSet.prototype.forEach = function (callback, options) {
  4291. var filter = options && options.filter,
  4292. convert = options && options.convert || this.options.convert,
  4293. data = this.data,
  4294. item,
  4295. id;
  4296. if (options && options.order) {
  4297. // execute forEach on ordered list
  4298. var items = this.get(options);
  4299. for (var i = 0, len = items.length; i < len; i++) {
  4300. item = items[i];
  4301. id = item[this.fieldId];
  4302. callback(item, id);
  4303. }
  4304. }
  4305. else {
  4306. // unordered
  4307. for (id in data) {
  4308. if (data.hasOwnProperty(id)) {
  4309. item = this._getItem(id, convert);
  4310. if (!filter || filter(item)) {
  4311. callback(item, id);
  4312. }
  4313. }
  4314. }
  4315. }
  4316. };
  4317. /**
  4318. * Map every item in the dataset.
  4319. * @param {function} callback
  4320. * @param {Object} [options] Available options:
  4321. * {Object.<String, String>} [convert]
  4322. * {String[]} [fields] filter fields
  4323. * {function} [filter] filter items
  4324. * {String | function} [order] Order the items by
  4325. * a field name or custom sort function.
  4326. * @return {Object[]} mappedItems
  4327. */
  4328. DataSet.prototype.map = function (callback, options) {
  4329. var filter = options && options.filter,
  4330. convert = options && options.convert || this.options.convert,
  4331. mappedItems = [],
  4332. data = this.data,
  4333. item;
  4334. // convert and filter items
  4335. for (var id in data) {
  4336. if (data.hasOwnProperty(id)) {
  4337. item = this._getItem(id, convert);
  4338. if (!filter || filter(item)) {
  4339. mappedItems.push(callback(item, id));
  4340. }
  4341. }
  4342. }
  4343. // order items
  4344. if (options && options.order) {
  4345. this._sort(mappedItems, options.order);
  4346. }
  4347. return mappedItems;
  4348. };
  4349. /**
  4350. * Filter the fields of an item
  4351. * @param {Object} item
  4352. * @param {String[]} fields Field names
  4353. * @return {Object} filteredItem
  4354. * @private
  4355. */
  4356. DataSet.prototype._filterFields = function (item, fields) {
  4357. var filteredItem = {};
  4358. for (var field in item) {
  4359. if (item.hasOwnProperty(field) && (fields.indexOf(field) != -1)) {
  4360. filteredItem[field] = item[field];
  4361. }
  4362. }
  4363. return filteredItem;
  4364. };
  4365. /**
  4366. * Sort the provided array with items
  4367. * @param {Object[]} items
  4368. * @param {String | function} order A field name or custom sort function.
  4369. * @private
  4370. */
  4371. DataSet.prototype._sort = function (items, order) {
  4372. if (util.isString(order)) {
  4373. // order by provided field name
  4374. var name = order; // field name
  4375. items.sort(function (a, b) {
  4376. var av = a[name];
  4377. var bv = b[name];
  4378. return (av > bv) ? 1 : ((av < bv) ? -1 : 0);
  4379. });
  4380. }
  4381. else if (typeof order === 'function') {
  4382. // order by sort function
  4383. items.sort(order);
  4384. }
  4385. // TODO: extend order by an Object {field:String, direction:String}
  4386. // where direction can be 'asc' or 'desc'
  4387. else {
  4388. throw new TypeError('Order must be a function or a string');
  4389. }
  4390. };
  4391. /**
  4392. * Remove an object by pointer or by id
  4393. * @param {String | Number | Object | Array} id Object or id, or an array with
  4394. * objects or ids to be removed
  4395. * @param {String} [senderId] Optional sender id
  4396. * @return {Array} removedIds
  4397. */
  4398. DataSet.prototype.remove = function (id, senderId) {
  4399. var removedIds = [],
  4400. i, len, removedId;
  4401. if (id instanceof Array) {
  4402. for (i = 0, len = id.length; i < len; i++) {
  4403. removedId = this._remove(id[i]);
  4404. if (removedId != null) {
  4405. removedIds.push(removedId);
  4406. }
  4407. }
  4408. }
  4409. else {
  4410. removedId = this._remove(id);
  4411. if (removedId != null) {
  4412. removedIds.push(removedId);
  4413. }
  4414. }
  4415. if (removedIds.length) {
  4416. this._trigger('remove', {items: removedIds}, senderId);
  4417. }
  4418. return removedIds;
  4419. };
  4420. /**
  4421. * Remove an item by its id
  4422. * @param {Number | String | Object} id id or item
  4423. * @returns {Number | String | null} id
  4424. * @private
  4425. */
  4426. DataSet.prototype._remove = function (id) {
  4427. if (util.isNumber(id) || util.isString(id)) {
  4428. if (this.data[id]) {
  4429. delete this.data[id];
  4430. delete this.internalIds[id];
  4431. return id;
  4432. }
  4433. }
  4434. else if (id instanceof Object) {
  4435. var itemId = id[this.fieldId];
  4436. if (itemId && this.data[itemId]) {
  4437. delete this.data[itemId];
  4438. delete this.internalIds[itemId];
  4439. return itemId;
  4440. }
  4441. }
  4442. return null;
  4443. };
  4444. /**
  4445. * Clear the data
  4446. * @param {String} [senderId] Optional sender id
  4447. * @return {Array} removedIds The ids of all removed items
  4448. */
  4449. DataSet.prototype.clear = function (senderId) {
  4450. var ids = Object.keys(this.data);
  4451. this.data = {};
  4452. this.internalIds = {};
  4453. this._trigger('remove', {items: ids}, senderId);
  4454. return ids;
  4455. };
  4456. /**
  4457. * Find the item with maximum value of a specified field
  4458. * @param {String} field
  4459. * @return {Object | null} item Item containing max value, or null if no items
  4460. */
  4461. DataSet.prototype.max = function (field) {
  4462. var data = this.data,
  4463. max = null,
  4464. maxField = null;
  4465. for (var id in data) {
  4466. if (data.hasOwnProperty(id)) {
  4467. var item = data[id];
  4468. var itemField = item[field];
  4469. if (itemField != null && (!max || itemField > maxField)) {
  4470. max = item;
  4471. maxField = itemField;
  4472. }
  4473. }
  4474. }
  4475. return max;
  4476. };
  4477. /**
  4478. * Find the item with minimum value of a specified field
  4479. * @param {String} field
  4480. * @return {Object | null} item Item containing max value, or null if no items
  4481. */
  4482. DataSet.prototype.min = function (field) {
  4483. var data = this.data,
  4484. min = null,
  4485. minField = null;
  4486. for (var id in data) {
  4487. if (data.hasOwnProperty(id)) {
  4488. var item = data[id];
  4489. var itemField = item[field];
  4490. if (itemField != null && (!min || itemField < minField)) {
  4491. min = item;
  4492. minField = itemField;
  4493. }
  4494. }
  4495. }
  4496. return min;
  4497. };
  4498. /**
  4499. * Find all distinct values of a specified field
  4500. * @param {String} field
  4501. * @return {Array} values Array containing all distinct values. If the data
  4502. * items do not contain the specified field, an array
  4503. * containing a single value undefined is returned.
  4504. * The returned array is unordered.
  4505. */
  4506. DataSet.prototype.distinct = function (field) {
  4507. var data = this.data,
  4508. values = [],
  4509. fieldType = this.options.convert[field],
  4510. count = 0;
  4511. for (var prop in data) {
  4512. if (data.hasOwnProperty(prop)) {
  4513. var item = data[prop];
  4514. var value = util.convert(item[field], fieldType);
  4515. var exists = false;
  4516. for (var i = 0; i < count; i++) {
  4517. if (values[i] == value) {
  4518. exists = true;
  4519. break;
  4520. }
  4521. }
  4522. if (!exists) {
  4523. values[count] = value;
  4524. count++;
  4525. }
  4526. }
  4527. }
  4528. return values;
  4529. };
  4530. /**
  4531. * Add a single item. Will fail when an item with the same id already exists.
  4532. * @param {Object} item
  4533. * @return {String} id
  4534. * @private
  4535. */
  4536. DataSet.prototype._addItem = function (item) {
  4537. var id = item[this.fieldId];
  4538. if (id != undefined) {
  4539. // check whether this id is already taken
  4540. if (this.data[id]) {
  4541. // item already exists
  4542. throw new Error('Cannot add item: item with id ' + id + ' already exists');
  4543. }
  4544. }
  4545. else {
  4546. // generate an id
  4547. id = util.randomUUID();
  4548. item[this.fieldId] = id;
  4549. this.internalIds[id] = item;
  4550. }
  4551. var d = {};
  4552. for (var field in item) {
  4553. if (item.hasOwnProperty(field)) {
  4554. var fieldType = this.convert[field]; // type may be undefined
  4555. d[field] = util.convert(item[field], fieldType);
  4556. }
  4557. }
  4558. this.data[id] = d;
  4559. return id;
  4560. };
  4561. /**
  4562. * Get an item. Fields can be converted to a specific type
  4563. * @param {String} id
  4564. * @param {Object.<String, String>} [convert] field types to convert
  4565. * @return {Object | null} item
  4566. * @private
  4567. */
  4568. DataSet.prototype._getItem = function (id, convert) {
  4569. var field, value;
  4570. // get the item from the dataset
  4571. var raw = this.data[id];
  4572. if (!raw) {
  4573. return null;
  4574. }
  4575. // convert the items field types
  4576. var converted = {},
  4577. fieldId = this.fieldId,
  4578. internalIds = this.internalIds;
  4579. if (convert) {
  4580. for (field in raw) {
  4581. if (raw.hasOwnProperty(field)) {
  4582. value = raw[field];
  4583. // output all fields, except internal ids
  4584. if ((field != fieldId) || !(value in internalIds)) {
  4585. converted[field] = util.convert(value, convert[field]);
  4586. }
  4587. }
  4588. }
  4589. }
  4590. else {
  4591. // no field types specified, no converting needed
  4592. for (field in raw) {
  4593. if (raw.hasOwnProperty(field)) {
  4594. value = raw[field];
  4595. // output all fields, except internal ids
  4596. if ((field != fieldId) || !(value in internalIds)) {
  4597. converted[field] = value;
  4598. }
  4599. }
  4600. }
  4601. }
  4602. return converted;
  4603. };
  4604. /**
  4605. * Update a single item: merge with existing item.
  4606. * Will fail when the item has no id, or when there does not exist an item
  4607. * with the same id.
  4608. * @param {Object} item
  4609. * @return {String} id
  4610. * @private
  4611. */
  4612. DataSet.prototype._updateItem = function (item) {
  4613. var id = item[this.fieldId];
  4614. if (id == undefined) {
  4615. throw new Error('Cannot update item: item has no id (item: ' + JSON.stringify(item) + ')');
  4616. }
  4617. var d = this.data[id];
  4618. if (!d) {
  4619. // item doesn't exist
  4620. throw new Error('Cannot update item: no item with id ' + id + ' found');
  4621. }
  4622. // merge with current item
  4623. for (var field in item) {
  4624. if (item.hasOwnProperty(field)) {
  4625. var fieldType = this.convert[field]; // type may be undefined
  4626. d[field] = util.convert(item[field], fieldType);
  4627. }
  4628. }
  4629. return id;
  4630. };
  4631. /**
  4632. * Get an array with the column names of a Google DataTable
  4633. * @param {DataTable} dataTable
  4634. * @return {String[]} columnNames
  4635. * @private
  4636. */
  4637. DataSet.prototype._getColumnNames = function (dataTable) {
  4638. var columns = [];
  4639. for (var col = 0, cols = dataTable.getNumberOfColumns(); col < cols; col++) {
  4640. columns[col] = dataTable.getColumnId(col) || dataTable.getColumnLabel(col);
  4641. }
  4642. return columns;
  4643. };
  4644. /**
  4645. * Append an item as a row to the dataTable
  4646. * @param dataTable
  4647. * @param columns
  4648. * @param item
  4649. * @private
  4650. */
  4651. DataSet.prototype._appendRow = function (dataTable, columns, item) {
  4652. var row = dataTable.addRow();
  4653. for (var col = 0, cols = columns.length; col < cols; col++) {
  4654. var field = columns[col];
  4655. dataTable.setValue(row, col, item[field]);
  4656. }
  4657. };
  4658. /**
  4659. * DataView
  4660. *
  4661. * a dataview offers a filtered view on a dataset or an other dataview.
  4662. *
  4663. * @param {DataSet | DataView} data
  4664. * @param {Object} [options] Available options: see method get
  4665. *
  4666. * @constructor DataView
  4667. */
  4668. function DataView (data, options) {
  4669. this.id = util.randomUUID();
  4670. this.data = null;
  4671. this.ids = {}; // ids of the items currently in memory (just contains a boolean true)
  4672. this.options = options || {};
  4673. this.fieldId = 'id'; // name of the field containing id
  4674. this.subscribers = {}; // event subscribers
  4675. var me = this;
  4676. this.listener = function () {
  4677. me._onEvent.apply(me, arguments);
  4678. };
  4679. this.setData(data);
  4680. }
  4681. /**
  4682. * Set a data source for the view
  4683. * @param {DataSet | DataView} data
  4684. */
  4685. DataView.prototype.setData = function (data) {
  4686. var ids, dataItems, i, len;
  4687. if (this.data) {
  4688. // unsubscribe from current dataset
  4689. if (this.data.unsubscribe) {
  4690. this.data.unsubscribe('*', this.listener);
  4691. }
  4692. // trigger a remove of all items in memory
  4693. ids = [];
  4694. for (var id in this.ids) {
  4695. if (this.ids.hasOwnProperty(id)) {
  4696. ids.push(id);
  4697. }
  4698. }
  4699. this.ids = {};
  4700. this._trigger('remove', {items: ids});
  4701. }
  4702. this.data = data;
  4703. if (this.data) {
  4704. // update fieldId
  4705. this.fieldId = this.options.fieldId ||
  4706. (this.data && this.data.options && this.data.options.fieldId) ||
  4707. 'id';
  4708. // trigger an add of all added items
  4709. ids = this.data.getIds({filter: this.options && this.options.filter});
  4710. for (i = 0, len = ids.length; i < len; i++) {
  4711. id = ids[i];
  4712. this.ids[id] = true;
  4713. }
  4714. this._trigger('add', {items: ids});
  4715. // subscribe to new dataset
  4716. if (this.data.subscribe) {
  4717. this.data.subscribe('*', this.listener);
  4718. }
  4719. }
  4720. };
  4721. /**
  4722. * Get data from the data view
  4723. *
  4724. * Usage:
  4725. *
  4726. * get()
  4727. * get(options: Object)
  4728. * get(options: Object, data: Array | DataTable)
  4729. *
  4730. * get(id: Number)
  4731. * get(id: Number, options: Object)
  4732. * get(id: Number, options: Object, data: Array | DataTable)
  4733. *
  4734. * get(ids: Number[])
  4735. * get(ids: Number[], options: Object)
  4736. * get(ids: Number[], options: Object, data: Array | DataTable)
  4737. *
  4738. * Where:
  4739. *
  4740. * {Number | String} id The id of an item
  4741. * {Number[] | String{}} ids An array with ids of items
  4742. * {Object} options An Object with options. Available options:
  4743. * {String} [type] Type of data to be returned. Can
  4744. * be 'DataTable' or 'Array' (default)
  4745. * {Object.<String, String>} [convert]
  4746. * {String[]} [fields] field names to be returned
  4747. * {function} [filter] filter items
  4748. * {String | function} [order] Order the items by
  4749. * a field name or custom sort function.
  4750. * {Array | DataTable} [data] If provided, items will be appended to this
  4751. * array or table. Required in case of Google
  4752. * DataTable.
  4753. * @param args
  4754. */
  4755. DataView.prototype.get = function (args) {
  4756. var me = this;
  4757. // parse the arguments
  4758. var ids, options, data;
  4759. var firstType = util.getType(arguments[0]);
  4760. if (firstType == 'String' || firstType == 'Number' || firstType == 'Array') {
  4761. // get(id(s) [, options] [, data])
  4762. ids = arguments[0]; // can be a single id or an array with ids
  4763. options = arguments[1];
  4764. data = arguments[2];
  4765. }
  4766. else {
  4767. // get([, options] [, data])
  4768. options = arguments[0];
  4769. data = arguments[1];
  4770. }
  4771. // extend the options with the default options and provided options
  4772. var viewOptions = util.extend({}, this.options, options);
  4773. // create a combined filter method when needed
  4774. if (this.options.filter && options && options.filter) {
  4775. viewOptions.filter = function (item) {
  4776. return me.options.filter(item) && options.filter(item);
  4777. }
  4778. }
  4779. // build up the call to the linked data set
  4780. var getArguments = [];
  4781. if (ids != undefined) {
  4782. getArguments.push(ids);
  4783. }
  4784. getArguments.push(viewOptions);
  4785. getArguments.push(data);
  4786. return this.data && this.data.get.apply(this.data, getArguments);
  4787. };
  4788. /**
  4789. * Get ids of all items or from a filtered set of items.
  4790. * @param {Object} [options] An Object with options. Available options:
  4791. * {function} [filter] filter items
  4792. * {String | function} [order] Order the items by
  4793. * a field name or custom sort function.
  4794. * @return {Array} ids
  4795. */
  4796. DataView.prototype.getIds = function (options) {
  4797. var ids;
  4798. if (this.data) {
  4799. var defaultFilter = this.options.filter;
  4800. var filter;
  4801. if (options && options.filter) {
  4802. if (defaultFilter) {
  4803. filter = function (item) {
  4804. return defaultFilter(item) && options.filter(item);
  4805. }
  4806. }
  4807. else {
  4808. filter = options.filter;
  4809. }
  4810. }
  4811. else {
  4812. filter = defaultFilter;
  4813. }
  4814. ids = this.data.getIds({
  4815. filter: filter,
  4816. order: options && options.order
  4817. });
  4818. }
  4819. else {
  4820. ids = [];
  4821. }
  4822. return ids;
  4823. };
  4824. /**
  4825. * Event listener. Will propagate all events from the connected data set to
  4826. * the subscribers of the DataView, but will filter the items and only trigger
  4827. * when there are changes in the filtered data set.
  4828. * @param {String} event
  4829. * @param {Object | null} params
  4830. * @param {String} senderId
  4831. * @private
  4832. */
  4833. DataView.prototype._onEvent = function (event, params, senderId) {
  4834. var i, len, id, item,
  4835. ids = params && params.items,
  4836. data = this.data,
  4837. added = [],
  4838. updated = [],
  4839. removed = [];
  4840. if (ids && data) {
  4841. switch (event) {
  4842. case 'add':
  4843. // filter the ids of the added items
  4844. for (i = 0, len = ids.length; i < len; i++) {
  4845. id = ids[i];
  4846. item = this.get(id);
  4847. if (item) {
  4848. this.ids[id] = true;
  4849. added.push(id);
  4850. }
  4851. }
  4852. break;
  4853. case 'update':
  4854. // determine the event from the views viewpoint: an updated
  4855. // item can be added, updated, or removed from this view.
  4856. for (i = 0, len = ids.length; i < len; i++) {
  4857. id = ids[i];
  4858. item = this.get(id);
  4859. if (item) {
  4860. if (this.ids[id]) {
  4861. updated.push(id);
  4862. }
  4863. else {
  4864. this.ids[id] = true;
  4865. added.push(id);
  4866. }
  4867. }
  4868. else {
  4869. if (this.ids[id]) {
  4870. delete this.ids[id];
  4871. removed.push(id);
  4872. }
  4873. else {
  4874. // nothing interesting for me :-(
  4875. }
  4876. }
  4877. }
  4878. break;
  4879. case 'remove':
  4880. // filter the ids of the removed items
  4881. for (i = 0, len = ids.length; i < len; i++) {
  4882. id = ids[i];
  4883. if (this.ids[id]) {
  4884. delete this.ids[id];
  4885. removed.push(id);
  4886. }
  4887. }
  4888. break;
  4889. }
  4890. if (added.length) {
  4891. this._trigger('add', {items: added}, senderId);
  4892. }
  4893. if (updated.length) {
  4894. this._trigger('update', {items: updated}, senderId);
  4895. }
  4896. if (removed.length) {
  4897. this._trigger('remove', {items: removed}, senderId);
  4898. }
  4899. }
  4900. };
  4901. // copy subscription functionality from DataSet
  4902. DataView.prototype.subscribe = DataSet.prototype.subscribe;
  4903. DataView.prototype.unsubscribe = DataSet.prototype.unsubscribe;
  4904. DataView.prototype._trigger = DataSet.prototype._trigger;
  4905. /**
  4906. * @constructor TimeStep
  4907. * The class TimeStep is an iterator for dates. You provide a start date and an
  4908. * end date. The class itself determines the best scale (step size) based on the
  4909. * provided start Date, end Date, and minimumStep.
  4910. *
  4911. * If minimumStep is provided, the step size is chosen as close as possible
  4912. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4913. * provided, the scale is set to 1 DAY.
  4914. * The minimumStep should correspond with the onscreen size of about 6 characters
  4915. *
  4916. * Alternatively, you can set a scale by hand.
  4917. * After creation, you can initialize the class by executing first(). Then you
  4918. * can iterate from the start date to the end date via next(). You can check if
  4919. * the end date is reached with the function hasNext(). After each step, you can
  4920. * retrieve the current date via getCurrent().
  4921. * The TimeStep has scales ranging from milliseconds, seconds, minutes, hours,
  4922. * days, to years.
  4923. *
  4924. * Version: 1.2
  4925. *
  4926. * @param {Date} [start] The start date, for example new Date(2010, 9, 21)
  4927. * or new Date(2010, 9, 21, 23, 45, 00)
  4928. * @param {Date} [end] The end date
  4929. * @param {Number} [minimumStep] Optional. Minimum step size in milliseconds
  4930. */
  4931. TimeStep = function(start, end, minimumStep) {
  4932. // variables
  4933. this.current = new Date();
  4934. this._start = new Date();
  4935. this._end = new Date();
  4936. this.autoScale = true;
  4937. this.scale = TimeStep.SCALE.DAY;
  4938. this.step = 1;
  4939. // initialize the range
  4940. this.setRange(start, end, minimumStep);
  4941. };
  4942. /// enum scale
  4943. TimeStep.SCALE = {
  4944. MILLISECOND: 1,
  4945. SECOND: 2,
  4946. MINUTE: 3,
  4947. HOUR: 4,
  4948. DAY: 5,
  4949. WEEKDAY: 6,
  4950. MONTH: 7,
  4951. YEAR: 8
  4952. };
  4953. /**
  4954. * Set a new range
  4955. * If minimumStep is provided, the step size is chosen as close as possible
  4956. * to the minimumStep but larger than minimumStep. If minimumStep is not
  4957. * provided, the scale is set to 1 DAY.
  4958. * The minimumStep should correspond with the onscreen size of about 6 characters
  4959. * @param {Date} [start] The start date and time.
  4960. * @param {Date} [end] The end date and time.
  4961. * @param {int} [minimumStep] Optional. Minimum step size in milliseconds
  4962. */
  4963. TimeStep.prototype.setRange = function(start, end, minimumStep) {
  4964. if (!(start instanceof Date) || !(end instanceof Date)) {
  4965. //throw "No legal start or end date in method setRange";
  4966. return;
  4967. }
  4968. this._start = (start != undefined) ? new Date(start.valueOf()) : new Date();
  4969. this._end = (end != undefined) ? new Date(end.valueOf()) : new Date();
  4970. if (this.autoScale) {
  4971. this.setMinimumStep(minimumStep);
  4972. }
  4973. };
  4974. /**
  4975. * Set the range iterator to the start date.
  4976. */
  4977. TimeStep.prototype.first = function() {
  4978. this.current = new Date(this._start.valueOf());
  4979. this.roundToMinor();
  4980. };
  4981. /**
  4982. * Round the current date to the first minor date value
  4983. * This must be executed once when the current date is set to start Date
  4984. */
  4985. TimeStep.prototype.roundToMinor = function() {
  4986. // round to floor
  4987. // IMPORTANT: we have no breaks in this switch! (this is no bug)
  4988. //noinspection FallthroughInSwitchStatementJS
  4989. switch (this.scale) {
  4990. case TimeStep.SCALE.YEAR:
  4991. this.current.setFullYear(this.step * Math.floor(this.current.getFullYear() / this.step));
  4992. this.current.setMonth(0);
  4993. case TimeStep.SCALE.MONTH: this.current.setDate(1);
  4994. case TimeStep.SCALE.DAY: // intentional fall through
  4995. case TimeStep.SCALE.WEEKDAY: this.current.setHours(0);
  4996. case TimeStep.SCALE.HOUR: this.current.setMinutes(0);
  4997. case TimeStep.SCALE.MINUTE: this.current.setSeconds(0);
  4998. case TimeStep.SCALE.SECOND: this.current.setMilliseconds(0);
  4999. //case TimeStep.SCALE.MILLISECOND: // nothing to do for milliseconds
  5000. }
  5001. if (this.step != 1) {
  5002. // round down to the first minor value that is a multiple of the current step size
  5003. switch (this.scale) {
  5004. case TimeStep.SCALE.MILLISECOND: this.current.setMilliseconds(this.current.getMilliseconds() - this.current.getMilliseconds() % this.step); break;
  5005. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() - this.current.getSeconds() % this.step); break;
  5006. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() - this.current.getMinutes() % this.step); break;
  5007. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() - this.current.getHours() % this.step); break;
  5008. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5009. case TimeStep.SCALE.DAY: this.current.setDate((this.current.getDate()-1) - (this.current.getDate()-1) % this.step + 1); break;
  5010. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() - this.current.getMonth() % this.step); break;
  5011. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() - this.current.getFullYear() % this.step); break;
  5012. default: break;
  5013. }
  5014. }
  5015. };
  5016. /**
  5017. * Check if the there is a next step
  5018. * @return {boolean} true if the current date has not passed the end date
  5019. */
  5020. TimeStep.prototype.hasNext = function () {
  5021. return (this.current.valueOf() <= this._end.valueOf());
  5022. };
  5023. /**
  5024. * Do the next step
  5025. */
  5026. TimeStep.prototype.next = function() {
  5027. var prev = this.current.valueOf();
  5028. // Two cases, needed to prevent issues with switching daylight savings
  5029. // (end of March and end of October)
  5030. if (this.current.getMonth() < 6) {
  5031. switch (this.scale) {
  5032. case TimeStep.SCALE.MILLISECOND:
  5033. this.current = new Date(this.current.valueOf() + this.step); break;
  5034. case TimeStep.SCALE.SECOND: this.current = new Date(this.current.valueOf() + this.step * 1000); break;
  5035. case TimeStep.SCALE.MINUTE: this.current = new Date(this.current.valueOf() + this.step * 1000 * 60); break;
  5036. case TimeStep.SCALE.HOUR:
  5037. this.current = new Date(this.current.valueOf() + this.step * 1000 * 60 * 60);
  5038. // in case of skipping an hour for daylight savings, adjust the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
  5039. var h = this.current.getHours();
  5040. this.current.setHours(h - (h % this.step));
  5041. break;
  5042. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5043. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  5044. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  5045. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  5046. default: break;
  5047. }
  5048. }
  5049. else {
  5050. switch (this.scale) {
  5051. case TimeStep.SCALE.MILLISECOND: this.current = new Date(this.current.valueOf() + this.step); break;
  5052. case TimeStep.SCALE.SECOND: this.current.setSeconds(this.current.getSeconds() + this.step); break;
  5053. case TimeStep.SCALE.MINUTE: this.current.setMinutes(this.current.getMinutes() + this.step); break;
  5054. case TimeStep.SCALE.HOUR: this.current.setHours(this.current.getHours() + this.step); break;
  5055. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5056. case TimeStep.SCALE.DAY: this.current.setDate(this.current.getDate() + this.step); break;
  5057. case TimeStep.SCALE.MONTH: this.current.setMonth(this.current.getMonth() + this.step); break;
  5058. case TimeStep.SCALE.YEAR: this.current.setFullYear(this.current.getFullYear() + this.step); break;
  5059. default: break;
  5060. }
  5061. }
  5062. if (this.step != 1) {
  5063. // round down to the correct major value
  5064. switch (this.scale) {
  5065. case TimeStep.SCALE.MILLISECOND: if(this.current.getMilliseconds() < this.step) this.current.setMilliseconds(0); break;
  5066. case TimeStep.SCALE.SECOND: if(this.current.getSeconds() < this.step) this.current.setSeconds(0); break;
  5067. case TimeStep.SCALE.MINUTE: if(this.current.getMinutes() < this.step) this.current.setMinutes(0); break;
  5068. case TimeStep.SCALE.HOUR: if(this.current.getHours() < this.step) this.current.setHours(0); break;
  5069. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5070. case TimeStep.SCALE.DAY: if(this.current.getDate() < this.step+1) this.current.setDate(1); break;
  5071. case TimeStep.SCALE.MONTH: if(this.current.getMonth() < this.step) this.current.setMonth(0); break;
  5072. case TimeStep.SCALE.YEAR: break; // nothing to do for year
  5073. default: break;
  5074. }
  5075. }
  5076. // safety mechanism: if current time is still unchanged, move to the end
  5077. if (this.current.valueOf() == prev) {
  5078. this.current = new Date(this._end.valueOf());
  5079. }
  5080. };
  5081. /**
  5082. * Get the current datetime
  5083. * @return {Date} current The current date
  5084. */
  5085. TimeStep.prototype.getCurrent = function() {
  5086. return this.current;
  5087. };
  5088. /**
  5089. * Set a custom scale. Autoscaling will be disabled.
  5090. * For example setScale(SCALE.MINUTES, 5) will result
  5091. * in minor steps of 5 minutes, and major steps of an hour.
  5092. *
  5093. * @param {TimeStep.SCALE} newScale
  5094. * A scale. Choose from SCALE.MILLISECOND,
  5095. * SCALE.SECOND, SCALE.MINUTE, SCALE.HOUR,
  5096. * SCALE.WEEKDAY, SCALE.DAY, SCALE.MONTH,
  5097. * SCALE.YEAR.
  5098. * @param {Number} newStep A step size, by default 1. Choose for
  5099. * example 1, 2, 5, or 10.
  5100. */
  5101. TimeStep.prototype.setScale = function(newScale, newStep) {
  5102. this.scale = newScale;
  5103. if (newStep > 0) {
  5104. this.step = newStep;
  5105. }
  5106. this.autoScale = false;
  5107. };
  5108. /**
  5109. * Enable or disable autoscaling
  5110. * @param {boolean} enable If true, autoascaling is set true
  5111. */
  5112. TimeStep.prototype.setAutoScale = function (enable) {
  5113. this.autoScale = enable;
  5114. };
  5115. /**
  5116. * Automatically determine the scale that bests fits the provided minimum step
  5117. * @param {Number} [minimumStep] The minimum step size in milliseconds
  5118. */
  5119. TimeStep.prototype.setMinimumStep = function(minimumStep) {
  5120. if (minimumStep == undefined) {
  5121. return;
  5122. }
  5123. var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
  5124. var stepMonth = (1000 * 60 * 60 * 24 * 30);
  5125. var stepDay = (1000 * 60 * 60 * 24);
  5126. var stepHour = (1000 * 60 * 60);
  5127. var stepMinute = (1000 * 60);
  5128. var stepSecond = (1000);
  5129. var stepMillisecond= (1);
  5130. // find the smallest step that is larger than the provided minimumStep
  5131. if (stepYear*1000 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1000;}
  5132. if (stepYear*500 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 500;}
  5133. if (stepYear*100 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 100;}
  5134. if (stepYear*50 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 50;}
  5135. if (stepYear*10 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 10;}
  5136. if (stepYear*5 > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 5;}
  5137. if (stepYear > minimumStep) {this.scale = TimeStep.SCALE.YEAR; this.step = 1;}
  5138. if (stepMonth*3 > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 3;}
  5139. if (stepMonth > minimumStep) {this.scale = TimeStep.SCALE.MONTH; this.step = 1;}
  5140. if (stepDay*5 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 5;}
  5141. if (stepDay*2 > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 2;}
  5142. if (stepDay > minimumStep) {this.scale = TimeStep.SCALE.DAY; this.step = 1;}
  5143. if (stepDay/2 > minimumStep) {this.scale = TimeStep.SCALE.WEEKDAY; this.step = 1;}
  5144. if (stepHour*4 > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 4;}
  5145. if (stepHour > minimumStep) {this.scale = TimeStep.SCALE.HOUR; this.step = 1;}
  5146. if (stepMinute*15 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 15;}
  5147. if (stepMinute*10 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 10;}
  5148. if (stepMinute*5 > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 5;}
  5149. if (stepMinute > minimumStep) {this.scale = TimeStep.SCALE.MINUTE; this.step = 1;}
  5150. if (stepSecond*15 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 15;}
  5151. if (stepSecond*10 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 10;}
  5152. if (stepSecond*5 > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 5;}
  5153. if (stepSecond > minimumStep) {this.scale = TimeStep.SCALE.SECOND; this.step = 1;}
  5154. if (stepMillisecond*200 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 200;}
  5155. if (stepMillisecond*100 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 100;}
  5156. if (stepMillisecond*50 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 50;}
  5157. if (stepMillisecond*10 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 10;}
  5158. if (stepMillisecond*5 > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 5;}
  5159. if (stepMillisecond > minimumStep) {this.scale = TimeStep.SCALE.MILLISECOND; this.step = 1;}
  5160. };
  5161. /**
  5162. * Snap a date to a rounded value. The snap intervals are dependent on the
  5163. * current scale and step.
  5164. * @param {Date} date the date to be snapped
  5165. */
  5166. TimeStep.prototype.snap = function(date) {
  5167. if (this.scale == TimeStep.SCALE.YEAR) {
  5168. var year = date.getFullYear() + Math.round(date.getMonth() / 12);
  5169. date.setFullYear(Math.round(year / this.step) * this.step);
  5170. date.setMonth(0);
  5171. date.setDate(0);
  5172. date.setHours(0);
  5173. date.setMinutes(0);
  5174. date.setSeconds(0);
  5175. date.setMilliseconds(0);
  5176. }
  5177. else if (this.scale == TimeStep.SCALE.MONTH) {
  5178. if (date.getDate() > 15) {
  5179. date.setDate(1);
  5180. date.setMonth(date.getMonth() + 1);
  5181. // important: first set Date to 1, after that change the month.
  5182. }
  5183. else {
  5184. date.setDate(1);
  5185. }
  5186. date.setHours(0);
  5187. date.setMinutes(0);
  5188. date.setSeconds(0);
  5189. date.setMilliseconds(0);
  5190. }
  5191. else if (this.scale == TimeStep.SCALE.DAY ||
  5192. this.scale == TimeStep.SCALE.WEEKDAY) {
  5193. //noinspection FallthroughInSwitchStatementJS
  5194. switch (this.step) {
  5195. case 5:
  5196. case 2:
  5197. date.setHours(Math.round(date.getHours() / 24) * 24); break;
  5198. default:
  5199. date.setHours(Math.round(date.getHours() / 12) * 12); break;
  5200. }
  5201. date.setMinutes(0);
  5202. date.setSeconds(0);
  5203. date.setMilliseconds(0);
  5204. }
  5205. else if (this.scale == TimeStep.SCALE.HOUR) {
  5206. switch (this.step) {
  5207. case 4:
  5208. date.setMinutes(Math.round(date.getMinutes() / 60) * 60); break;
  5209. default:
  5210. date.setMinutes(Math.round(date.getMinutes() / 30) * 30); break;
  5211. }
  5212. date.setSeconds(0);
  5213. date.setMilliseconds(0);
  5214. } else if (this.scale == TimeStep.SCALE.MINUTE) {
  5215. //noinspection FallthroughInSwitchStatementJS
  5216. switch (this.step) {
  5217. case 15:
  5218. case 10:
  5219. date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
  5220. date.setSeconds(0);
  5221. break;
  5222. case 5:
  5223. date.setSeconds(Math.round(date.getSeconds() / 60) * 60); break;
  5224. default:
  5225. date.setSeconds(Math.round(date.getSeconds() / 30) * 30); break;
  5226. }
  5227. date.setMilliseconds(0);
  5228. }
  5229. else if (this.scale == TimeStep.SCALE.SECOND) {
  5230. //noinspection FallthroughInSwitchStatementJS
  5231. switch (this.step) {
  5232. case 15:
  5233. case 10:
  5234. date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
  5235. date.setMilliseconds(0);
  5236. break;
  5237. case 5:
  5238. date.setMilliseconds(Math.round(date.getMilliseconds() / 1000) * 1000); break;
  5239. default:
  5240. date.setMilliseconds(Math.round(date.getMilliseconds() / 500) * 500); break;
  5241. }
  5242. }
  5243. else if (this.scale == TimeStep.SCALE.MILLISECOND) {
  5244. var step = this.step > 5 ? this.step / 2 : 1;
  5245. date.setMilliseconds(Math.round(date.getMilliseconds() / step) * step);
  5246. }
  5247. };
  5248. /**
  5249. * Check if the current value is a major value (for example when the step
  5250. * is DAY, a major value is each first day of the MONTH)
  5251. * @return {boolean} true if current date is major, else false.
  5252. */
  5253. TimeStep.prototype.isMajor = function() {
  5254. switch (this.scale) {
  5255. case TimeStep.SCALE.MILLISECOND:
  5256. return (this.current.getMilliseconds() == 0);
  5257. case TimeStep.SCALE.SECOND:
  5258. return (this.current.getSeconds() == 0);
  5259. case TimeStep.SCALE.MINUTE:
  5260. return (this.current.getHours() == 0) && (this.current.getMinutes() == 0);
  5261. // Note: this is no bug. Major label is equal for both minute and hour scale
  5262. case TimeStep.SCALE.HOUR:
  5263. return (this.current.getHours() == 0);
  5264. case TimeStep.SCALE.WEEKDAY: // intentional fall through
  5265. case TimeStep.SCALE.DAY:
  5266. return (this.current.getDate() == 1);
  5267. case TimeStep.SCALE.MONTH:
  5268. return (this.current.getMonth() == 0);
  5269. case TimeStep.SCALE.YEAR:
  5270. return false;
  5271. default:
  5272. return false;
  5273. }
  5274. };
  5275. /**
  5276. * Returns formatted text for the minor axislabel, depending on the current
  5277. * date and the scale. For example when scale is MINUTE, the current time is
  5278. * formatted as "hh:mm".
  5279. * @param {Date} [date] custom date. if not provided, current date is taken
  5280. */
  5281. TimeStep.prototype.getLabelMinor = function(date) {
  5282. if (date == undefined) {
  5283. date = this.current;
  5284. }
  5285. switch (this.scale) {
  5286. case TimeStep.SCALE.MILLISECOND: return moment(date).format('SSS');
  5287. case TimeStep.SCALE.SECOND: return moment(date).format('s');
  5288. case TimeStep.SCALE.MINUTE: return moment(date).format('HH:mm');
  5289. case TimeStep.SCALE.HOUR: return moment(date).format('HH:mm');
  5290. case TimeStep.SCALE.WEEKDAY: return moment(date).format('ddd D');
  5291. case TimeStep.SCALE.DAY: return moment(date).format('D');
  5292. case TimeStep.SCALE.MONTH: return moment(date).format('MMM');
  5293. case TimeStep.SCALE.YEAR: return moment(date).format('YYYY');
  5294. default: return '';
  5295. }
  5296. };
  5297. /**
  5298. * Returns formatted text for the major axis label, depending on the current
  5299. * date and the scale. For example when scale is MINUTE, the major scale is
  5300. * hours, and the hour will be formatted as "hh".
  5301. * @param {Date} [date] custom date. if not provided, current date is taken
  5302. */
  5303. TimeStep.prototype.getLabelMajor = function(date) {
  5304. if (date == undefined) {
  5305. date = this.current;
  5306. }
  5307. //noinspection FallthroughInSwitchStatementJS
  5308. switch (this.scale) {
  5309. case TimeStep.SCALE.MILLISECOND:return moment(date).format('HH:mm:ss');
  5310. case TimeStep.SCALE.SECOND: return moment(date).format('D MMMM HH:mm');
  5311. case TimeStep.SCALE.MINUTE:
  5312. case TimeStep.SCALE.HOUR: return moment(date).format('ddd D MMMM');
  5313. case TimeStep.SCALE.WEEKDAY:
  5314. case TimeStep.SCALE.DAY: return moment(date).format('MMMM YYYY');
  5315. case TimeStep.SCALE.MONTH: return moment(date).format('YYYY');
  5316. case TimeStep.SCALE.YEAR: return '';
  5317. default: return '';
  5318. }
  5319. };
  5320. /**
  5321. * @constructor Stack
  5322. * Stacks items on top of each other.
  5323. * @param {ItemSet} parent
  5324. * @param {Object} [options]
  5325. */
  5326. function Stack (parent, options) {
  5327. this.parent = parent;
  5328. this.options = options || {};
  5329. this.defaultOptions = {
  5330. order: function (a, b) {
  5331. //return (b.width - a.width) || (a.left - b.left); // TODO: cleanup
  5332. // Order: ranges over non-ranges, ranged ordered by width, and
  5333. // lastly ordered by start.
  5334. if (a instanceof ItemRange) {
  5335. if (b instanceof ItemRange) {
  5336. var aInt = (a.data.end - a.data.start);
  5337. var bInt = (b.data.end - b.data.start);
  5338. return (aInt - bInt) || (a.data.start - b.data.start);
  5339. }
  5340. else {
  5341. return -1;
  5342. }
  5343. }
  5344. else {
  5345. if (b instanceof ItemRange) {
  5346. return 1;
  5347. }
  5348. else {
  5349. return (a.data.start - b.data.start);
  5350. }
  5351. }
  5352. },
  5353. margin: {
  5354. item: 10
  5355. }
  5356. };
  5357. this.ordered = []; // ordered items
  5358. }
  5359. /**
  5360. * Set options for the stack
  5361. * @param {Object} options Available options:
  5362. * {ItemSet} parent
  5363. * {Number} margin
  5364. * {function} order Stacking order
  5365. */
  5366. Stack.prototype.setOptions = function setOptions (options) {
  5367. util.extend(this.options, options);
  5368. // TODO: register on data changes at the connected parent itemset, and update the changed part only and immediately
  5369. };
  5370. /**
  5371. * Stack the items such that they don't overlap. The items will have a minimal
  5372. * distance equal to options.margin.item.
  5373. */
  5374. Stack.prototype.update = function update() {
  5375. this._order();
  5376. this._stack();
  5377. };
  5378. /**
  5379. * Order the items. The items are ordered by width first, and by left position
  5380. * second.
  5381. * If a custom order function has been provided via the options, then this will
  5382. * be used.
  5383. * @private
  5384. */
  5385. Stack.prototype._order = function _order () {
  5386. var items = this.parent.items;
  5387. if (!items) {
  5388. throw new Error('Cannot stack items: parent does not contain items');
  5389. }
  5390. // TODO: store the sorted items, to have less work later on
  5391. var ordered = [];
  5392. var index = 0;
  5393. // items is a map (no array)
  5394. util.forEach(items, function (item) {
  5395. if (item.visible) {
  5396. ordered[index] = item;
  5397. index++;
  5398. }
  5399. });
  5400. //if a customer stack order function exists, use it.
  5401. var order = this.options.order || this.defaultOptions.order;
  5402. if (!(typeof order === 'function')) {
  5403. throw new Error('Option order must be a function');
  5404. }
  5405. ordered.sort(order);
  5406. this.ordered = ordered;
  5407. };
  5408. /**
  5409. * Adjust vertical positions of the events such that they don't overlap each
  5410. * other.
  5411. * @private
  5412. */
  5413. Stack.prototype._stack = function _stack () {
  5414. var i,
  5415. iMax,
  5416. ordered = this.ordered,
  5417. options = this.options,
  5418. orientation = options.orientation || this.defaultOptions.orientation,
  5419. axisOnTop = (orientation == 'top'),
  5420. margin;
  5421. if (options.margin && options.margin.item !== undefined) {
  5422. margin = options.margin.item;
  5423. }
  5424. else {
  5425. margin = this.defaultOptions.margin.item
  5426. }
  5427. // calculate new, non-overlapping positions
  5428. for (i = 0, iMax = ordered.length; i < iMax; i++) {
  5429. var item = ordered[i];
  5430. var collidingItem = null;
  5431. do {
  5432. // TODO: optimize checking for overlap. when there is a gap without items,
  5433. // you only need to check for items from the next item on, not from zero
  5434. collidingItem = this.checkOverlap(ordered, i, 0, i - 1, margin);
  5435. if (collidingItem != null) {
  5436. // There is a collision. Reposition the event above the colliding element
  5437. if (axisOnTop) {
  5438. item.top = collidingItem.top + collidingItem.height + margin;
  5439. }
  5440. else {
  5441. item.top = collidingItem.top - item.height - margin;
  5442. }
  5443. }
  5444. } while (collidingItem);
  5445. }
  5446. };
  5447. /**
  5448. * Check if the destiny position of given item overlaps with any
  5449. * of the other items from index itemStart to itemEnd.
  5450. * @param {Array} items Array with items
  5451. * @param {int} itemIndex Number of the item to be checked for overlap
  5452. * @param {int} itemStart First item to be checked.
  5453. * @param {int} itemEnd Last item to be checked.
  5454. * @return {Object | null} colliding item, or undefined when no collisions
  5455. * @param {Number} margin A minimum required margin.
  5456. * If margin is provided, the two items will be
  5457. * marked colliding when they overlap or
  5458. * when the margin between the two is smaller than
  5459. * the requested margin.
  5460. */
  5461. Stack.prototype.checkOverlap = function checkOverlap (items, itemIndex,
  5462. itemStart, itemEnd, margin) {
  5463. var collision = this.collision;
  5464. // we loop from end to start, as we suppose that the chance of a
  5465. // collision is larger for items at the end, so check these first.
  5466. var a = items[itemIndex];
  5467. for (var i = itemEnd; i >= itemStart; i--) {
  5468. var b = items[i];
  5469. if (collision(a, b, margin)) {
  5470. if (i != itemIndex) {
  5471. return b;
  5472. }
  5473. }
  5474. }
  5475. return null;
  5476. };
  5477. /**
  5478. * Test if the two provided items collide
  5479. * The items must have parameters left, width, top, and height.
  5480. * @param {Component} a The first item
  5481. * @param {Component} b The second item
  5482. * @param {Number} margin A minimum required margin.
  5483. * If margin is provided, the two items will be
  5484. * marked colliding when they overlap or
  5485. * when the margin between the two is smaller than
  5486. * the requested margin.
  5487. * @return {boolean} true if a and b collide, else false
  5488. */
  5489. Stack.prototype.collision = function collision (a, b, margin) {
  5490. return ((a.left - margin) < (b.left + b.width) &&
  5491. (a.left + a.width + margin) > b.left &&
  5492. (a.top - margin) < (b.top + b.height) &&
  5493. (a.top + a.height + margin) > b.top);
  5494. };
  5495. /**
  5496. * @constructor Range
  5497. * A Range controls a numeric range with a start and end value.
  5498. * The Range adjusts the range based on mouse events or programmatic changes,
  5499. * and triggers events when the range is changing or has been changed.
  5500. * @param {Object} [options] See description at Range.setOptions
  5501. * @extends Controller
  5502. */
  5503. function Range(options) {
  5504. this.id = util.randomUUID();
  5505. this.start = 0; // Number
  5506. this.end = 0; // Number
  5507. // this.options = options || {}; // TODO: fix range options
  5508. this.options = {
  5509. min: null,
  5510. max: null,
  5511. zoomMin: null,
  5512. zoomMax: null
  5513. };
  5514. this.listeners = [];
  5515. this.setOptions(options);
  5516. }
  5517. /**
  5518. * Set options for the range controller
  5519. * @param {Object} options Available options:
  5520. * {Number} start Set start value of the range
  5521. * {Number} end Set end value of the range
  5522. * {Number} min Minimum value for start
  5523. * {Number} max Maximum value for end
  5524. * {Number} zoomMin Set a minimum value for
  5525. * (end - start).
  5526. * {Number} zoomMax Set a maximum value for
  5527. * (end - start).
  5528. */
  5529. Range.prototype.setOptions = function (options) {
  5530. util.extend(this.options, options);
  5531. if (options.start != null || options.end != null) {
  5532. this.setRange(options.start, options.end);
  5533. }
  5534. };
  5535. /**
  5536. * Add listeners for mouse and touch events to the component
  5537. * @param {Component} component
  5538. * @param {String} event Available events: 'move', 'zoom'
  5539. * @param {String} direction Available directions: 'horizontal', 'vertical'
  5540. */
  5541. Range.prototype.subscribe = function (component, event, direction) {
  5542. var me = this;
  5543. var listener;
  5544. if (direction != 'horizontal' && direction != 'vertical') {
  5545. throw new TypeError('Unknown direction "' + direction + '". ' +
  5546. 'Choose "horizontal" or "vertical".');
  5547. }
  5548. //noinspection FallthroughInSwitchStatementJS
  5549. if (event == 'move') {
  5550. listener = {
  5551. component: component,
  5552. event: event,
  5553. direction: direction,
  5554. callback: function (event) {
  5555. me._onMouseDown(event, listener);
  5556. },
  5557. params: {}
  5558. };
  5559. component.on('mousedown', listener.callback);
  5560. me.listeners.push(listener);
  5561. }
  5562. else if (event == 'zoom') {
  5563. listener = {
  5564. component: component,
  5565. event: event,
  5566. direction: direction,
  5567. callback: function (event) {
  5568. me._onMouseWheel(event, listener);
  5569. },
  5570. params: {}
  5571. };
  5572. component.on('mousewheel', listener.callback);
  5573. me.listeners.push(listener);
  5574. }
  5575. else {
  5576. throw new TypeError('Unknown event "' + event + '". ' +
  5577. 'Choose "move" or "zoom".');
  5578. }
  5579. };
  5580. /**
  5581. * Event handler
  5582. * @param {String} event name of the event, for example 'click', 'mousemove'
  5583. * @param {function} callback callback handler, invoked with the raw HTML Event
  5584. * as parameter.
  5585. */
  5586. Range.prototype.on = function (event, callback) {
  5587. events.addListener(this, event, callback);
  5588. };
  5589. /**
  5590. * Trigger an event
  5591. * @param {String} event name of the event, available events: 'rangechange',
  5592. * 'rangechanged'
  5593. * @private
  5594. */
  5595. Range.prototype._trigger = function (event) {
  5596. events.trigger(this, event, {
  5597. start: this.start,
  5598. end: this.end
  5599. });
  5600. };
  5601. /**
  5602. * Set a new start and end range
  5603. * @param {Number} start
  5604. * @param {Number} end
  5605. */
  5606. Range.prototype.setRange = function(start, end) {
  5607. var changed = this._applyRange(start, end);
  5608. if (changed) {
  5609. this._trigger('rangechange');
  5610. this._trigger('rangechanged');
  5611. }
  5612. };
  5613. /**
  5614. * Set a new start and end range. This method is the same as setRange, but
  5615. * does not trigger a range change and range changed event, and it returns
  5616. * true when the range is changed
  5617. * @param {Number} start
  5618. * @param {Number} end
  5619. * @return {Boolean} changed
  5620. * @private
  5621. */
  5622. Range.prototype._applyRange = function(start, end) {
  5623. var newStart = (start != null) ? util.convert(start, 'Number') : this.start;
  5624. var newEnd = (end != null) ? util.convert(end, 'Number') : this.end;
  5625. var diff;
  5626. // check for valid number
  5627. if (isNaN(newStart)) {
  5628. throw new Error('Invalid start "' + start + '"');
  5629. }
  5630. if (isNaN(newEnd)) {
  5631. throw new Error('Invalid end "' + end + '"');
  5632. }
  5633. // prevent start < end
  5634. if (newEnd < newStart) {
  5635. newEnd = newStart;
  5636. }
  5637. <<<<<<< HEAD
  5638. this._updateConversion();
  5639. var me = this,
  5640. queue = this.queue,
  5641. itemsData = this.itemsData,
  5642. items = this.items,
  5643. dataOptions = {
  5644. // TODO: cleanup
  5645. //fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type']
  5646. };
  5647. // show/hide added/changed/removed items
  5648. Object.keys(queue).forEach(function (id) {
  5649. //var entry = queue[id];
  5650. var action = queue[id];
  5651. var item = items[id];
  5652. //var item = entry.item;
  5653. //noinspection FallthroughInSwitchStatementJS
  5654. switch (action) {
  5655. case 'add':
  5656. case 'update':
  5657. var itemData = itemsData && itemsData.get(id, dataOptions);
  5658. if (itemData) {
  5659. var type = itemData.type ||
  5660. (itemData.start && itemData.end && 'range') ||
  5661. options.type ||
  5662. 'box';
  5663. var constructor = ItemSet.types[type];
  5664. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  5665. if (item) {
  5666. // update item
  5667. if (!constructor || !(item instanceof constructor)) {
  5668. // item type has changed, hide and delete the item
  5669. changed += item.hide();
  5670. item = null;
  5671. }
  5672. else {
  5673. item.data = itemData; // TODO: create a method item.setData ?
  5674. changed++;
  5675. }
  5676. }
  5677. if (!item) {
  5678. // create item
  5679. if (constructor) {
  5680. item = new constructor(me, itemData, options, defaultOptions);
  5681. changed++;
  5682. }
  5683. else {
  5684. throw new TypeError('Unknown item type "' + type + '"');
  5685. }
  5686. }
  5687. // force a repaint (not only a reposition)
  5688. item.repaint();
  5689. items[id] = item;
  5690. }
  5691. // update queue
  5692. delete queue[id];
  5693. break;
  5694. case 'remove':
  5695. if (item) {
  5696. // remove DOM of the item
  5697. changed += item.hide();
  5698. }
  5699. =======
  5700. // prevent start < min
  5701. if (this.options.min != null) {
  5702. var min = this.options.min.valueOf();
  5703. if (newStart < min) {
  5704. diff = (min - newStart);
  5705. newStart += diff;
  5706. newEnd += diff;
  5707. }
  5708. }
  5709. >>>>>>> upstream/develop
  5710. // prevent end > max
  5711. if (this.options.max != null) {
  5712. var max = this.options.max.valueOf();
  5713. if (newEnd > max) {
  5714. diff = (newEnd - max);
  5715. newStart -= diff;
  5716. newEnd -= diff;
  5717. }
  5718. }
  5719. // prevent (end-start) > zoomMin
  5720. if (this.options.zoomMin != null) {
  5721. var zoomMin = this.options.zoomMin.valueOf();
  5722. if (zoomMin < 0) {
  5723. zoomMin = 0;
  5724. }
  5725. if ((newEnd - newStart) < zoomMin) {
  5726. if ((this.end - this.start) > zoomMin) {
  5727. // zoom to the minimum
  5728. diff = (zoomMin - (newEnd - newStart));
  5729. newStart -= diff / 2;
  5730. newEnd += diff / 2;
  5731. }
  5732. else {
  5733. // ingore this action, we are already zoomed to the minimum
  5734. newStart = this.start;
  5735. newEnd = this.end;
  5736. }
  5737. }
  5738. }
  5739. // prevent (end-start) > zoomMin
  5740. if (this.options.zoomMax != null) {
  5741. var zoomMax = this.options.zoomMax.valueOf();
  5742. if (zoomMax < 0) {
  5743. zoomMax = 0;
  5744. }
  5745. if ((newEnd - newStart) > zoomMax) {
  5746. if ((this.end - this.start) < zoomMax) {
  5747. // zoom to the maximum
  5748. diff = ((newEnd - newStart) - zoomMax);
  5749. newStart += diff / 2;
  5750. newEnd -= diff / 2;
  5751. }
  5752. else {
  5753. // ingore this action, we are already zoomed to the maximum
  5754. newStart = this.start;
  5755. newEnd = this.end;
  5756. }
  5757. }
  5758. }
  5759. var changed = (this.start != newStart || this.end != newEnd);
  5760. this.start = newStart;
  5761. this.end = newEnd;
  5762. return changed;
  5763. };
  5764. /**
  5765. * Retrieve the current range.
  5766. * @return {Object} An object with start and end properties
  5767. */
  5768. Range.prototype.getRange = function() {
  5769. return {
  5770. start: this.start,
  5771. end: this.end
  5772. };
  5773. };
  5774. /**
  5775. * Calculate the conversion offset and factor for current range, based on
  5776. * the provided width
  5777. * @param {Number} width
  5778. * @returns {{offset: number, factor: number}} conversion
  5779. */
  5780. Range.prototype.conversion = function (width) {
  5781. var start = this.start;
  5782. var end = this.end;
  5783. return Range.conversion(this.start, this.end, width);
  5784. };
  5785. /**
  5786. * Static method to calculate the conversion offset and factor for a range,
  5787. * based on the provided start, end, and width
  5788. * @param {Number} start
  5789. * @param {Number} end
  5790. * @param {Number} width
  5791. * @returns {{offset: number, factor: number}} conversion
  5792. */
  5793. Range.conversion = function (start, end, width) {
  5794. if (width != 0 && (end - start != 0)) {
  5795. return {
  5796. offset: start,
  5797. factor: width / (end - start)
  5798. }
  5799. }
  5800. else {
  5801. return {
  5802. offset: 0,
  5803. factor: 1
  5804. };
  5805. }
  5806. };
  5807. /**
  5808. * Start moving horizontally or vertically
  5809. * @param {Event} event
  5810. * @param {Object} listener Listener containing the component and params
  5811. * @private
  5812. */
  5813. Range.prototype._onMouseDown = function(event, listener) {
  5814. event = event || window.event;
  5815. var params = listener.params;
  5816. // only react on left mouse button down
  5817. var leftButtonDown = event.which ? (event.which == 1) : (event.button == 1);
  5818. if (!leftButtonDown) {
  5819. return;
  5820. }
  5821. // get mouse position
  5822. params.mouseX = util.getPageX(event);
  5823. params.mouseY = util.getPageY(event);
  5824. params.previousLeft = 0;
  5825. params.previousOffset = 0;
  5826. params.moved = false;
  5827. params.start = this.start;
  5828. params.end = this.end;
  5829. var frame = listener.component.frame;
  5830. if (frame) {
  5831. frame.style.cursor = 'move';
  5832. }
  5833. // add event listeners to handle moving the contents
  5834. // we store the function onmousemove and onmouseup in the timeaxis,
  5835. // so we can remove the eventlisteners lateron in the function onmouseup
  5836. var me = this;
  5837. if (!params.onMouseMove) {
  5838. params.onMouseMove = function (event) {
  5839. me._onMouseMove(event, listener);
  5840. };
  5841. util.addEventListener(document, "mousemove", params.onMouseMove);
  5842. }
  5843. if (!params.onMouseUp) {
  5844. params.onMouseUp = function (event) {
  5845. me._onMouseUp(event, listener);
  5846. };
  5847. util.addEventListener(document, "mouseup", params.onMouseUp);
  5848. }
  5849. util.preventDefault(event);
  5850. };
  5851. /**
  5852. * Perform moving operating.
  5853. * This function activated from within the funcion TimeAxis._onMouseDown().
  5854. * @param {Event} event
  5855. * @param {Object} listener
  5856. * @private
  5857. */
  5858. Range.prototype._onMouseMove = function (event, listener) {
  5859. event = event || window.event;
  5860. var params = listener.params;
  5861. // calculate change in mouse position
  5862. var mouseX = util.getPageX(event);
  5863. var mouseY = util.getPageY(event);
  5864. if (params.mouseX == undefined) {
  5865. params.mouseX = mouseX;
  5866. }
  5867. if (params.mouseY == undefined) {
  5868. params.mouseY = mouseY;
  5869. }
  5870. var diffX = mouseX - params.mouseX;
  5871. var diffY = mouseY - params.mouseY;
  5872. var diff = (listener.direction == 'horizontal') ? diffX : diffY;
  5873. // if mouse movement is big enough, register it as a "moved" event
  5874. if (Math.abs(diff) >= 1) {
  5875. params.moved = true;
  5876. }
  5877. var interval = (params.end - params.start);
  5878. var width = (listener.direction == 'horizontal') ?
  5879. listener.component.width : listener.component.height;
  5880. var diffRange = -diff / width * interval;
  5881. this._applyRange(params.start + diffRange, params.end + diffRange);
  5882. // fire a rangechange event
  5883. this._trigger('rangechange');
  5884. util.preventDefault(event);
  5885. };
  5886. /**
  5887. * Stop moving operating.
  5888. * This function activated from within the function Range._onMouseDown().
  5889. * @param {event} event
  5890. * @param {Object} listener
  5891. * @private
  5892. */
  5893. Range.prototype._onMouseUp = function (event, listener) {
  5894. event = event || window.event;
  5895. var params = listener.params;
  5896. if (listener.component.frame) {
  5897. listener.component.frame.style.cursor = 'auto';
  5898. }
  5899. // remove event listeners here, important for Safari
  5900. if (params.onMouseMove) {
  5901. util.removeEventListener(document, "mousemove", params.onMouseMove);
  5902. params.onMouseMove = null;
  5903. }
  5904. if (params.onMouseUp) {
  5905. util.removeEventListener(document, "mouseup", params.onMouseUp);
  5906. params.onMouseUp = null;
  5907. }
  5908. //util.preventDefault(event);
  5909. if (params.moved) {
  5910. // fire a rangechanged event
  5911. this._trigger('rangechanged');
  5912. }
  5913. };
  5914. /**
  5915. * Event handler for mouse wheel event, used to zoom
  5916. * Code from http://adomas.org/javascript-mouse-wheel/
  5917. * @param {Event} event
  5918. * @param {Object} listener
  5919. * @private
  5920. */
  5921. Range.prototype._onMouseWheel = function(event, listener) {
  5922. event = event || window.event;
  5923. // retrieve delta
  5924. var delta = 0;
  5925. if (event.wheelDelta) { /* IE/Opera. */
  5926. delta = event.wheelDelta / 120;
  5927. } else if (event.detail) { /* Mozilla case. */
  5928. // In Mozilla, sign of delta is different than in IE.
  5929. // Also, delta is multiple of 3.
  5930. delta = -event.detail / 3;
  5931. }
  5932. // If delta is nonzero, handle it.
  5933. // Basically, delta is now positive if wheel was scrolled up,
  5934. // and negative, if wheel was scrolled down.
  5935. if (delta) {
  5936. var me = this;
  5937. var zoom = function () {
  5938. // perform the zoom action. Delta is normally 1 or -1
  5939. var zoomFactor = delta / 5.0;
  5940. var zoomAround = null;
  5941. var frame = listener.component.frame;
  5942. if (frame) {
  5943. var size, conversion;
  5944. if (listener.direction == 'horizontal') {
  5945. size = listener.component.width;
  5946. conversion = me.conversion(size);
  5947. var frameLeft = util.getAbsoluteLeft(frame);
  5948. var mouseX = util.getPageX(event);
  5949. zoomAround = (mouseX - frameLeft) / conversion.factor + conversion.offset;
  5950. }
  5951. else {
  5952. size = listener.component.height;
  5953. conversion = me.conversion(size);
  5954. var frameTop = util.getAbsoluteTop(frame);
  5955. var mouseY = util.getPageY(event);
  5956. zoomAround = ((frameTop + size - mouseY) - frameTop) / conversion.factor + conversion.offset;
  5957. }
  5958. }
  5959. me.zoom(zoomFactor, zoomAround);
  5960. };
  5961. zoom();
  5962. }
  5963. // Prevent default actions caused by mouse wheel.
  5964. // That might be ugly, but we handle scrolls somehow
  5965. // anyway, so don't bother here...
  5966. util.preventDefault(event);
  5967. };
  5968. /**
  5969. * Zoom the range the given zoomfactor in or out. Start and end date will
  5970. * be adjusted, and the timeline will be redrawn. You can optionally give a
  5971. * date around which to zoom.
  5972. * For example, try zoomfactor = 0.1 or -0.1
  5973. * @param {Number} zoomFactor Zooming amount. Positive value will zoom in,
  5974. * negative value will zoom out
  5975. * @param {Number} zoomAround Value around which will be zoomed. Optional
  5976. */
  5977. Range.prototype.zoom = function(zoomFactor, zoomAround) {
  5978. // if zoomAroundDate is not provided, take it half between start Date and end Date
  5979. if (zoomAround == null) {
  5980. zoomAround = (this.start + this.end) / 2;
  5981. }
  5982. // prevent zoom factor larger than 1 or smaller than -1 (larger than 1 will
  5983. // result in a start>=end )
  5984. if (zoomFactor >= 1) {
  5985. zoomFactor = 0.9;
  5986. }
  5987. if (zoomFactor <= -1) {
  5988. zoomFactor = -0.9;
  5989. }
  5990. // adjust a negative factor such that zooming in with 0.1 equals zooming
  5991. // out with a factor -0.1
  5992. if (zoomFactor < 0) {
  5993. zoomFactor = zoomFactor / (1 + zoomFactor);
  5994. }
  5995. // zoom start and end relative to the zoomAround value
  5996. var startDiff = (this.start - zoomAround);
  5997. var endDiff = (this.end - zoomAround);
  5998. // calculate new start and end
  5999. var newStart = this.start - startDiff * zoomFactor;
  6000. var newEnd = this.end - endDiff * zoomFactor;
  6001. this.setRange(newStart, newEnd);
  6002. };
  6003. /**
  6004. * Move the range with a given factor to the left or right. Start and end
  6005. * value will be adjusted. For example, try moveFactor = 0.1 or -0.1
  6006. * @param {Number} moveFactor Moving amount. Positive value will move right,
  6007. * negative value will move left
  6008. */
  6009. Range.prototype.move = function(moveFactor) {
  6010. // zoom start Date and end Date relative to the zoomAroundDate
  6011. var diff = (this.end - this.start);
  6012. // apply new values
  6013. var newStart = this.start + diff * moveFactor;
  6014. var newEnd = this.end + diff * moveFactor;
  6015. // TODO: reckon with min and max range
  6016. this.start = newStart;
  6017. this.end = newEnd;
  6018. };
  6019. /**
  6020. * Move the range to a new center point
  6021. * @param {Number} moveTo New center point of the range
  6022. */
  6023. Range.prototype.moveTo = function(moveTo) {
  6024. var center = (this.start + this.end) / 2;
  6025. var diff = center - moveTo;
  6026. // calculate new start and end
  6027. var newStart = this.start - diff;
  6028. var newEnd = this.end - diff;
  6029. this.setRange(newStart, newEnd);
  6030. }
  6031. /**
  6032. * @constructor Controller
  6033. *
  6034. * A Controller controls the reflows and repaints of all visual components
  6035. */
  6036. function Controller () {
  6037. this.id = util.randomUUID();
  6038. this.components = {};
  6039. this.repaintTimer = undefined;
  6040. this.reflowTimer = undefined;
  6041. }
  6042. /**
  6043. * Add a component to the controller
  6044. * @param {Component} component
  6045. */
  6046. Controller.prototype.add = function add(component) {
  6047. // validate the component
  6048. if (component.id == undefined) {
  6049. throw new Error('Component has no field id');
  6050. }
  6051. if (!(component instanceof Component) && !(component instanceof Controller)) {
  6052. throw new TypeError('Component must be an instance of ' +
  6053. 'prototype Component or Controller');
  6054. }
  6055. // add the component
  6056. component.controller = this;
  6057. this.components[component.id] = component;
  6058. };
  6059. /**
  6060. * Remove a component from the controller
  6061. * @param {Component | String} component
  6062. */
  6063. Controller.prototype.remove = function remove(component) {
  6064. var id;
  6065. for (id in this.components) {
  6066. if (this.components.hasOwnProperty(id)) {
  6067. if (id == component || this.components[id] == component) {
  6068. break;
  6069. }
  6070. }
  6071. }
  6072. if (id) {
  6073. delete this.components[id];
  6074. }
  6075. };
  6076. /**
  6077. * Request a reflow. The controller will schedule a reflow
  6078. * @param {Boolean} [force] If true, an immediate reflow is forced. Default
  6079. * is false.
  6080. */
  6081. Controller.prototype.requestReflow = function requestReflow(force) {
  6082. if (force) {
  6083. this.reflow();
  6084. }
  6085. else {
  6086. if (!this.reflowTimer) {
  6087. var me = this;
  6088. this.reflowTimer = setTimeout(function () {
  6089. me.reflowTimer = undefined;
  6090. me.reflow();
  6091. }, 0);
  6092. }
  6093. }
  6094. };
  6095. /**
  6096. * Request a repaint. The controller will schedule a repaint
  6097. * @param {Boolean} [force] If true, an immediate repaint is forced. Default
  6098. * is false.
  6099. */
  6100. Controller.prototype.requestRepaint = function requestRepaint(force) {
  6101. if (force) {
  6102. this.repaint();
  6103. }
  6104. else {
  6105. if (!this.repaintTimer) {
  6106. var me = this;
  6107. this.repaintTimer = setTimeout(function () {
  6108. me.repaintTimer = undefined;
  6109. me.repaint();
  6110. }, 0);
  6111. }
  6112. }
  6113. };
  6114. /**
  6115. * Repaint all components
  6116. */
  6117. Controller.prototype.repaint = function repaint() {
  6118. var changed = false;
  6119. // cancel any running repaint request
  6120. if (this.repaintTimer) {
  6121. clearTimeout(this.repaintTimer);
  6122. this.repaintTimer = undefined;
  6123. }
  6124. var done = {};
  6125. function repaint(component, id) {
  6126. if (!(id in done)) {
  6127. // first repaint the components on which this component is dependent
  6128. if (component.depends) {
  6129. component.depends.forEach(function (dep) {
  6130. repaint(dep, dep.id);
  6131. });
  6132. }
  6133. if (component.parent) {
  6134. repaint(component.parent, component.parent.id);
  6135. }
  6136. // repaint the component itself and mark as done
  6137. changed = component.repaint() || changed;
  6138. done[id] = true;
  6139. }
  6140. }
  6141. util.forEach(this.components, repaint);
  6142. // immediately reflow when needed
  6143. if (changed) {
  6144. this.reflow();
  6145. }
  6146. // TODO: limit the number of nested reflows/repaints, prevent loop
  6147. };
  6148. /**
  6149. * Reflow all components
  6150. */
  6151. Controller.prototype.reflow = function reflow() {
  6152. var resized = false;
  6153. // cancel any running repaint request
  6154. if (this.reflowTimer) {
  6155. clearTimeout(this.reflowTimer);
  6156. this.reflowTimer = undefined;
  6157. }
  6158. var done = {};
  6159. function reflow(component, id) {
  6160. if (!(id in done)) {
  6161. // first reflow the components on which this component is dependent
  6162. if (component.depends) {
  6163. component.depends.forEach(function (dep) {
  6164. reflow(dep, dep.id);
  6165. });
  6166. }
  6167. if (component.parent) {
  6168. reflow(component.parent, component.parent.id);
  6169. }
  6170. // reflow the component itself and mark as done
  6171. resized = component.reflow() || resized;
  6172. done[id] = true;
  6173. }
  6174. }
  6175. util.forEach(this.components, reflow);
  6176. // immediately repaint when needed
  6177. if (resized) {
  6178. this.repaint();
  6179. }
  6180. // TODO: limit the number of nested reflows/repaints, prevent loop
  6181. };
  6182. /**
  6183. * Prototype for visual components
  6184. */
  6185. function Component () {
  6186. this.id = null;
  6187. this.parent = null;
  6188. this.depends = null;
  6189. this.controller = null;
  6190. this.options = null;
  6191. this.frame = null; // main DOM element
  6192. this.top = 0;
  6193. this.left = 0;
  6194. this.width = 0;
  6195. this.height = 0;
  6196. }
  6197. /**
  6198. * Set parameters for the frame. Parameters will be merged in current parameter
  6199. * set.
  6200. * @param {Object} options Available parameters:
  6201. * {String | function} [className]
  6202. * {EventBus} [eventBus]
  6203. * {String | Number | function} [left]
  6204. * {String | Number | function} [top]
  6205. * {String | Number | function} [width]
  6206. * {String | Number | function} [height]
  6207. */
  6208. Component.prototype.setOptions = function setOptions(options) {
  6209. if (options) {
  6210. util.extend(this.options, options);
  6211. if (this.controller) {
  6212. this.requestRepaint();
  6213. this.requestReflow();
  6214. }
  6215. }
  6216. };
  6217. /**
  6218. * Get an option value by name
  6219. * The function will first check this.options object, and else will check
  6220. * this.defaultOptions.
  6221. * @param {String} name
  6222. * @return {*} value
  6223. */
  6224. Component.prototype.getOption = function getOption(name) {
  6225. var value;
  6226. if (this.options) {
  6227. value = this.options[name];
  6228. }
  6229. if (value === undefined && this.defaultOptions) {
  6230. value = this.defaultOptions[name];
  6231. }
  6232. return value;
  6233. };
  6234. /**
  6235. * Get the container element of the component, which can be used by a child to
  6236. * add its own widgets. Not all components do have a container for childs, in
  6237. * that case null is returned.
  6238. * @returns {HTMLElement | null} container
  6239. */
  6240. // TODO: get rid of the getContainer and getFrame methods, provide these via the options
  6241. Component.prototype.getContainer = function getContainer() {
  6242. // should be implemented by the component
  6243. return null;
  6244. };
  6245. /**
  6246. * Get the frame element of the component, the outer HTML DOM element.
  6247. * @returns {HTMLElement | null} frame
  6248. */
  6249. Component.prototype.getFrame = function getFrame() {
  6250. return this.frame;
  6251. };
  6252. /**
  6253. * Repaint the component
  6254. * @return {Boolean} changed
  6255. */
  6256. Component.prototype.repaint = function repaint() {
  6257. // should be implemented by the component
  6258. return false;
  6259. };
  6260. /**
  6261. * Reflow the component
  6262. * @return {Boolean} resized
  6263. */
  6264. Component.prototype.reflow = function reflow() {
  6265. // should be implemented by the component
  6266. return false;
  6267. };
  6268. /**
  6269. * Hide the component from the DOM
  6270. * @return {Boolean} changed
  6271. */
  6272. Component.prototype.hide = function hide() {
  6273. if (this.frame && this.frame.parentNode) {
  6274. this.frame.parentNode.removeChild(this.frame);
  6275. return true;
  6276. }
  6277. else {
  6278. return false;
  6279. }
  6280. };
  6281. /**
  6282. * Show the component in the DOM (when not already visible).
  6283. * A repaint will be executed when the component is not visible
  6284. * @return {Boolean} changed
  6285. */
  6286. Component.prototype.show = function show() {
  6287. if (!this.frame || !this.frame.parentNode) {
  6288. return this.repaint();
  6289. }
  6290. else {
  6291. return false;
  6292. }
  6293. };
  6294. /**
  6295. * Request a repaint. The controller will schedule a repaint
  6296. */
  6297. Component.prototype.requestRepaint = function requestRepaint() {
  6298. if (this.controller) {
  6299. this.controller.requestRepaint();
  6300. }
  6301. else {
  6302. throw new Error('Cannot request a repaint: no controller configured');
  6303. // TODO: just do a repaint when no parent is configured?
  6304. }
  6305. };
  6306. /**
  6307. * Request a reflow. The controller will schedule a reflow
  6308. */
  6309. Component.prototype.requestReflow = function requestReflow() {
  6310. if (this.controller) {
  6311. this.controller.requestReflow();
  6312. }
  6313. else {
  6314. throw new Error('Cannot request a reflow: no controller configured');
  6315. // TODO: just do a reflow when no parent is configured?
  6316. }
  6317. };
  6318. /**
  6319. * A panel can contain components
  6320. * @param {Component} [parent]
  6321. * @param {Component[]} [depends] Components on which this components depends
  6322. * (except for the parent)
  6323. * @param {Object} [options] Available parameters:
  6324. * {String | Number | function} [left]
  6325. * {String | Number | function} [top]
  6326. * {String | Number | function} [width]
  6327. * {String | Number | function} [height]
  6328. * {String | function} [className]
  6329. * @constructor Panel
  6330. * @extends Component
  6331. */
  6332. function Panel(parent, depends, options) {
  6333. this.id = util.randomUUID();
  6334. this.parent = parent;
  6335. this.depends = depends;
  6336. this.options = options || {};
  6337. }
  6338. Panel.prototype = new Component();
  6339. /**
  6340. * Set options. Will extend the current options.
  6341. * @param {Object} [options] Available parameters:
  6342. * {String | function} [className]
  6343. * {String | Number | function} [left]
  6344. * {String | Number | function} [top]
  6345. * {String | Number | function} [width]
  6346. * {String | Number | function} [height]
  6347. */
  6348. Panel.prototype.setOptions = Component.prototype.setOptions;
  6349. /**
  6350. * Get the container element of the panel, which can be used by a child to
  6351. * add its own widgets.
  6352. * @returns {HTMLElement} container
  6353. */
  6354. Panel.prototype.getContainer = function () {
  6355. return this.frame;
  6356. };
  6357. /**
  6358. * Repaint the component
  6359. * @return {Boolean} changed
  6360. */
  6361. Panel.prototype.repaint = function () {
  6362. var changed = 0,
  6363. update = util.updateProperty,
  6364. asSize = util.option.asSize,
  6365. options = this.options,
  6366. frame = this.frame;
  6367. if (!frame) {
  6368. frame = document.createElement('div');
  6369. frame.className = 'panel';
  6370. var className = options.className;
  6371. if (className) {
  6372. if (typeof className == 'function') {
  6373. util.addClassName(frame, String(className()));
  6374. }
  6375. else {
  6376. util.addClassName(frame, String(className));
  6377. }
  6378. }
  6379. this.frame = frame;
  6380. changed += 1;
  6381. }
  6382. if (!frame.parentNode) {
  6383. if (!this.parent) {
  6384. throw new Error('Cannot repaint panel: no parent attached');
  6385. }
  6386. var parentContainer = this.parent.getContainer();
  6387. if (!parentContainer) {
  6388. throw new Error('Cannot repaint panel: parent has no container element');
  6389. }
  6390. parentContainer.appendChild(frame);
  6391. changed += 1;
  6392. }
  6393. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6394. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6395. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6396. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  6397. return (changed > 0);
  6398. };
  6399. /**
  6400. * Reflow the component
  6401. * @return {Boolean} resized
  6402. */
  6403. Panel.prototype.reflow = function () {
  6404. var changed = 0,
  6405. update = util.updateProperty,
  6406. frame = this.frame;
  6407. if (frame) {
  6408. changed += update(this, 'top', frame.offsetTop);
  6409. changed += update(this, 'left', frame.offsetLeft);
  6410. changed += update(this, 'width', frame.offsetWidth);
  6411. changed += update(this, 'height', frame.offsetHeight);
  6412. }
  6413. else {
  6414. changed += 1;
  6415. }
  6416. return (changed > 0);
  6417. };
  6418. /**
  6419. * A root panel can hold components. The root panel must be initialized with
  6420. * a DOM element as container.
  6421. * @param {HTMLElement} container
  6422. * @param {Object} [options] Available parameters: see RootPanel.setOptions.
  6423. * @constructor RootPanel
  6424. * @extends Panel
  6425. */
  6426. function RootPanel(container, options) {
  6427. this.id = util.randomUUID();
  6428. this.container = container;
  6429. this.options = options || {};
  6430. this.defaultOptions = {
  6431. autoResize: true
  6432. };
  6433. this.listeners = {}; // event listeners
  6434. }
  6435. RootPanel.prototype = new Panel();
  6436. /**
  6437. * Set options. Will extend the current options.
  6438. * @param {Object} [options] Available parameters:
  6439. * {String | function} [className]
  6440. * {String | Number | function} [left]
  6441. * {String | Number | function} [top]
  6442. * {String | Number | function} [width]
  6443. * {String | Number | function} [height]
  6444. * {Boolean | function} [autoResize]
  6445. */
  6446. RootPanel.prototype.setOptions = Component.prototype.setOptions;
  6447. /**
  6448. * Repaint the component
  6449. * @return {Boolean} changed
  6450. */
  6451. RootPanel.prototype.repaint = function () {
  6452. var changed = 0,
  6453. update = util.updateProperty,
  6454. asSize = util.option.asSize,
  6455. options = this.options,
  6456. frame = this.frame;
  6457. if (!frame) {
  6458. frame = document.createElement('div');
  6459. frame.className = 'vis timeline rootpanel';
  6460. var className = options.className;
  6461. if (className) {
  6462. util.addClassName(frame, util.option.asString(className));
  6463. }
  6464. this.frame = frame;
  6465. changed += 1;
  6466. }
  6467. if (!frame.parentNode) {
  6468. if (!this.container) {
  6469. throw new Error('Cannot repaint root panel: no container attached');
  6470. }
  6471. this.container.appendChild(frame);
  6472. changed += 1;
  6473. }
  6474. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  6475. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6476. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6477. changed += update(frame.style, 'height', asSize(options.height, '100%'));
  6478. this._updateEventEmitters();
  6479. this._updateWatch();
  6480. return (changed > 0);
  6481. };
  6482. /**
  6483. * Reflow the component
  6484. * @return {Boolean} resized
  6485. */
  6486. RootPanel.prototype.reflow = function () {
  6487. var changed = 0,
  6488. update = util.updateProperty,
  6489. frame = this.frame;
  6490. if (frame) {
  6491. changed += update(this, 'top', frame.offsetTop);
  6492. changed += update(this, 'left', frame.offsetLeft);
  6493. changed += update(this, 'width', frame.offsetWidth);
  6494. changed += update(this, 'height', frame.offsetHeight);
  6495. }
  6496. else {
  6497. changed += 1;
  6498. }
  6499. return (changed > 0);
  6500. };
  6501. /**
  6502. * Update watching for resize, depending on the current option
  6503. * @private
  6504. */
  6505. RootPanel.prototype._updateWatch = function () {
  6506. var autoResize = this.getOption('autoResize');
  6507. if (autoResize) {
  6508. this._watch();
  6509. }
  6510. else {
  6511. this._unwatch();
  6512. }
  6513. };
  6514. /**
  6515. * Watch for changes in the size of the frame. On resize, the Panel will
  6516. * automatically redraw itself.
  6517. * @private
  6518. */
  6519. RootPanel.prototype._watch = function () {
  6520. var me = this;
  6521. this._unwatch();
  6522. var checkSize = function () {
  6523. var autoResize = me.getOption('autoResize');
  6524. if (!autoResize) {
  6525. // stop watching when the option autoResize is changed to false
  6526. me._unwatch();
  6527. return;
  6528. }
  6529. if (me.frame) {
  6530. // check whether the frame is resized
  6531. if ((me.frame.clientWidth != me.width) ||
  6532. (me.frame.clientHeight != me.height)) {
  6533. me.requestReflow();
  6534. }
  6535. }
  6536. };
  6537. // TODO: automatically cleanup the event listener when the frame is deleted
  6538. util.addEventListener(window, 'resize', checkSize);
  6539. this.watchTimer = setInterval(checkSize, 1000);
  6540. };
  6541. /**
  6542. * Stop watching for a resize of the frame.
  6543. * @private
  6544. */
  6545. RootPanel.prototype._unwatch = function () {
  6546. if (this.watchTimer) {
  6547. clearInterval(this.watchTimer);
  6548. this.watchTimer = undefined;
  6549. }
  6550. // TODO: remove event listener on window.resize
  6551. };
  6552. /**
  6553. * Event handler
  6554. * @param {String} event name of the event, for example 'click', 'mousemove'
  6555. * @param {function} callback callback handler, invoked with the raw HTML Event
  6556. * as parameter.
  6557. */
  6558. RootPanel.prototype.on = function (event, callback) {
  6559. // register the listener at this component
  6560. var arr = this.listeners[event];
  6561. if (!arr) {
  6562. arr = [];
  6563. this.listeners[event] = arr;
  6564. }
  6565. arr.push(callback);
  6566. this._updateEventEmitters();
  6567. };
  6568. /**
  6569. * Update the event listeners for all event emitters
  6570. * @private
  6571. */
  6572. RootPanel.prototype._updateEventEmitters = function () {
  6573. if (this.listeners) {
  6574. var me = this;
  6575. util.forEach(this.listeners, function (listeners, event) {
  6576. if (!me.emitters) {
  6577. me.emitters = {};
  6578. }
  6579. if (!(event in me.emitters)) {
  6580. // create event
  6581. var frame = me.frame;
  6582. if (frame) {
  6583. //console.log('Created a listener for event ' + event + ' on component ' + me.id); // TODO: cleanup logging
  6584. var callback = function(event) {
  6585. listeners.forEach(function (listener) {
  6586. // TODO: filter on event target!
  6587. listener(event);
  6588. });
  6589. };
  6590. me.emitters[event] = callback;
  6591. util.addEventListener(frame, event, callback);
  6592. }
  6593. }
  6594. });
  6595. // TODO: be able to delete event listeners
  6596. // TODO: be able to move event listeners to a parent when available
  6597. }
  6598. };
  6599. /**
  6600. * A horizontal time axis
  6601. * @param {Component} parent
  6602. * @param {Component[]} [depends] Components on which this components depends
  6603. * (except for the parent)
  6604. * @param {Object} [options] See TimeAxis.setOptions for the available
  6605. * options.
  6606. * @constructor TimeAxis
  6607. * @extends Component
  6608. */
  6609. function TimeAxis (parent, depends, options) {
  6610. this.id = util.randomUUID();
  6611. this.parent = parent;
  6612. this.depends = depends;
  6613. this.dom = {
  6614. majorLines: [],
  6615. majorTexts: [],
  6616. minorLines: [],
  6617. minorTexts: [],
  6618. redundant: {
  6619. majorLines: [],
  6620. majorTexts: [],
  6621. minorLines: [],
  6622. minorTexts: []
  6623. }
  6624. };
  6625. this.props = {
  6626. range: {
  6627. start: 0,
  6628. end: 0,
  6629. minimumStep: 0
  6630. },
  6631. lineTop: 0
  6632. };
  6633. this.options = options || {};
  6634. this.defaultOptions = {
  6635. orientation: 'bottom', // supported: 'top', 'bottom'
  6636. // TODO: implement timeaxis orientations 'left' and 'right'
  6637. showMinorLabels: true,
  6638. showMajorLabels: true
  6639. };
  6640. this.conversion = null;
  6641. this.range = null;
  6642. }
  6643. TimeAxis.prototype = new Component();
  6644. // TODO: comment options
  6645. TimeAxis.prototype.setOptions = Component.prototype.setOptions;
  6646. /**
  6647. * Set a range (start and end)
  6648. * @param {Range | Object} range A Range or an object containing start and end.
  6649. */
  6650. TimeAxis.prototype.setRange = function (range) {
  6651. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  6652. throw new TypeError('Range must be an instance of Range, ' +
  6653. 'or an object containing start and end.');
  6654. }
  6655. this.range = range;
  6656. };
  6657. /**
  6658. * Convert a position on screen (pixels) to a datetime
  6659. * @param {int} x Position on the screen in pixels
  6660. * @return {Date} time The datetime the corresponds with given position x
  6661. */
  6662. TimeAxis.prototype.toTime = function(x) {
  6663. var conversion = this.conversion;
  6664. return new Date(x / conversion.factor + conversion.offset);
  6665. };
  6666. /**
  6667. * Convert a datetime (Date object) into a position on the screen
  6668. * @param {Date} time A date
  6669. * @return {int} x The position on the screen in pixels which corresponds
  6670. * with the given date.
  6671. * @private
  6672. */
  6673. TimeAxis.prototype.toScreen = function(time) {
  6674. var conversion = this.conversion;
  6675. return (time.valueOf() - conversion.offset) * conversion.factor;
  6676. };
  6677. /**
  6678. * Repaint the component
  6679. * @return {Boolean} changed
  6680. */
  6681. TimeAxis.prototype.repaint = function () {
  6682. var changed = 0,
  6683. update = util.updateProperty,
  6684. asSize = util.option.asSize,
  6685. options = this.options,
  6686. orientation = this.getOption('orientation'),
  6687. props = this.props,
  6688. step = this.step;
  6689. var frame = this.frame;
  6690. if (!frame) {
  6691. frame = document.createElement('div');
  6692. this.frame = frame;
  6693. changed += 1;
  6694. }
  6695. frame.className = 'axis ' + orientation;
  6696. // TODO: custom className?
  6697. if (!frame.parentNode) {
  6698. if (!this.parent) {
  6699. throw new Error('Cannot repaint time axis: no parent attached');
  6700. }
  6701. var parentContainer = this.parent.getContainer();
  6702. if (!parentContainer) {
  6703. throw new Error('Cannot repaint time axis: parent has no container element');
  6704. }
  6705. parentContainer.appendChild(frame);
  6706. changed += 1;
  6707. }
  6708. var parent = frame.parentNode;
  6709. if (parent) {
  6710. var beforeChild = frame.nextSibling;
  6711. parent.removeChild(frame); // take frame offline while updating (is almost twice as fast)
  6712. var defaultTop = (orientation == 'bottom' && this.props.parentHeight && this.height) ?
  6713. (this.props.parentHeight - this.height) + 'px' :
  6714. '0px';
  6715. changed += update(frame.style, 'top', asSize(options.top, defaultTop));
  6716. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  6717. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  6718. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  6719. // get characters width and height
  6720. this._repaintMeasureChars();
  6721. if (this.step) {
  6722. this._repaintStart();
  6723. step.first();
  6724. var xFirstMajorLabel = undefined;
  6725. var max = 0;
  6726. while (step.hasNext() && max < 1000) {
  6727. max++;
  6728. var cur = step.getCurrent(),
  6729. x = this.toScreen(cur),
  6730. isMajor = step.isMajor();
  6731. // TODO: lines must have a width, such that we can create css backgrounds
  6732. if (this.getOption('showMinorLabels')) {
  6733. this._repaintMinorText(x, step.getLabelMinor());
  6734. }
  6735. if (isMajor && this.getOption('showMajorLabels')) {
  6736. if (x > 0) {
  6737. if (xFirstMajorLabel == undefined) {
  6738. xFirstMajorLabel = x;
  6739. }
  6740. this._repaintMajorText(x, step.getLabelMajor());
  6741. }
  6742. this._repaintMajorLine(x);
  6743. }
  6744. else {
  6745. this._repaintMinorLine(x);
  6746. }
  6747. step.next();
  6748. }
  6749. // create a major label on the left when needed
  6750. if (this.getOption('showMajorLabels')) {
  6751. var leftTime = this.toTime(0),
  6752. leftText = step.getLabelMajor(leftTime),
  6753. widthText = leftText.length * (props.majorCharWidth || 10) + 10; // upper bound estimation
  6754. if (xFirstMajorLabel == undefined || widthText < xFirstMajorLabel) {
  6755. this._repaintMajorText(0, leftText);
  6756. }
  6757. }
  6758. this._repaintEnd();
  6759. }
  6760. this._repaintLine();
  6761. // put frame online again
  6762. if (beforeChild) {
  6763. parent.insertBefore(frame, beforeChild);
  6764. }
  6765. else {
  6766. parent.appendChild(frame)
  6767. }
  6768. }
  6769. return (changed > 0);
  6770. };
  6771. /**
  6772. * Start a repaint. Move all DOM elements to a redundant list, where they
  6773. * can be picked for re-use, or can be cleaned up in the end
  6774. * @private
  6775. */
  6776. TimeAxis.prototype._repaintStart = function () {
  6777. var dom = this.dom,
  6778. redundant = dom.redundant;
  6779. redundant.majorLines = dom.majorLines;
  6780. redundant.majorTexts = dom.majorTexts;
  6781. redundant.minorLines = dom.minorLines;
  6782. redundant.minorTexts = dom.minorTexts;
  6783. dom.majorLines = [];
  6784. dom.majorTexts = [];
  6785. dom.minorLines = [];
  6786. dom.minorTexts = [];
  6787. };
  6788. /**
  6789. * End a repaint. Cleanup leftover DOM elements in the redundant list
  6790. * @private
  6791. */
  6792. TimeAxis.prototype._repaintEnd = function () {
  6793. util.forEach(this.dom.redundant, function (arr) {
  6794. while (arr.length) {
  6795. var elem = arr.pop();
  6796. if (elem && elem.parentNode) {
  6797. elem.parentNode.removeChild(elem);
  6798. }
  6799. }
  6800. });
  6801. };
  6802. /**
  6803. * Create a minor label for the axis at position x
  6804. * @param {Number} x
  6805. * @param {String} text
  6806. * @private
  6807. */
  6808. TimeAxis.prototype._repaintMinorText = function (x, text) {
  6809. // reuse redundant label
  6810. var label = this.dom.redundant.minorTexts.shift();
  6811. if (!label) {
  6812. // create new label
  6813. var content = document.createTextNode('');
  6814. label = document.createElement('div');
  6815. label.appendChild(content);
  6816. label.className = 'text minor';
  6817. this.frame.appendChild(label);
  6818. }
  6819. this.dom.minorTexts.push(label);
  6820. label.childNodes[0].nodeValue = text;
  6821. label.style.left = x + 'px';
  6822. label.style.top = this.props.minorLabelTop + 'px';
  6823. //label.title = title; // TODO: this is a heavy operation
  6824. };
  6825. /**
  6826. * Create a Major label for the axis at position x
  6827. * @param {Number} x
  6828. * @param {String} text
  6829. * @private
  6830. */
  6831. TimeAxis.prototype._repaintMajorText = function (x, text) {
  6832. // reuse redundant label
  6833. var label = this.dom.redundant.majorTexts.shift();
  6834. if (!label) {
  6835. // create label
  6836. var content = document.createTextNode(text);
  6837. label = document.createElement('div');
  6838. label.className = 'text major';
  6839. label.appendChild(content);
  6840. this.frame.appendChild(label);
  6841. }
  6842. this.dom.majorTexts.push(label);
  6843. label.childNodes[0].nodeValue = text;
  6844. label.style.top = this.props.majorLabelTop + 'px';
  6845. label.style.left = x + 'px';
  6846. //label.title = title; // TODO: this is a heavy operation
  6847. };
  6848. /**
  6849. * Create a minor line for the axis at position x
  6850. * @param {Number} x
  6851. * @private
  6852. */
  6853. TimeAxis.prototype._repaintMinorLine = function (x) {
  6854. // reuse redundant line
  6855. var line = this.dom.redundant.minorLines.shift();
  6856. if (!line) {
  6857. // create vertical line
  6858. line = document.createElement('div');
  6859. line.className = 'grid vertical minor';
  6860. this.frame.appendChild(line);
  6861. }
  6862. this.dom.minorLines.push(line);
  6863. var props = this.props;
  6864. line.style.top = props.minorLineTop + 'px';
  6865. line.style.height = props.minorLineHeight + 'px';
  6866. line.style.left = (x - props.minorLineWidth / 2) + 'px';
  6867. };
  6868. /**
  6869. * Create a Major line for the axis at position x
  6870. * @param {Number} x
  6871. * @private
  6872. */
  6873. TimeAxis.prototype._repaintMajorLine = function (x) {
  6874. // reuse redundant line
  6875. var line = this.dom.redundant.majorLines.shift();
  6876. if (!line) {
  6877. // create vertical line
  6878. line = document.createElement('DIV');
  6879. line.className = 'grid vertical major';
  6880. this.frame.appendChild(line);
  6881. }
  6882. this.dom.majorLines.push(line);
  6883. var props = this.props;
  6884. line.style.top = props.majorLineTop + 'px';
  6885. line.style.left = (x - props.majorLineWidth / 2) + 'px';
  6886. line.style.height = props.majorLineHeight + 'px';
  6887. };
  6888. /**
  6889. * Repaint the horizontal line for the axis
  6890. * @private
  6891. */
  6892. TimeAxis.prototype._repaintLine = function() {
  6893. var line = this.dom.line,
  6894. frame = this.frame,
  6895. options = this.options;
  6896. // line before all axis elements
  6897. if (this.getOption('showMinorLabels') || this.getOption('showMajorLabels')) {
  6898. if (line) {
  6899. // put this line at the end of all childs
  6900. frame.removeChild(line);
  6901. frame.appendChild(line);
  6902. }
  6903. else {
  6904. // create the axis line
  6905. line = document.createElement('div');
  6906. line.className = 'grid horizontal major';
  6907. frame.appendChild(line);
  6908. this.dom.line = line;
  6909. }
  6910. line.style.top = this.props.lineTop + 'px';
  6911. }
  6912. else {
  6913. if (line && axis.parentElement) {
  6914. frame.removeChild(axis.line);
  6915. delete this.dom.line;
  6916. }
  6917. }
  6918. };
  6919. /**
  6920. * Create characters used to determine the size of text on the axis
  6921. * @private
  6922. */
  6923. TimeAxis.prototype._repaintMeasureChars = function () {
  6924. // calculate the width and height of a single character
  6925. // this is used to calculate the step size, and also the positioning of the
  6926. // axis
  6927. var dom = this.dom,
  6928. text;
  6929. if (!dom.measureCharMinor) {
  6930. text = document.createTextNode('0');
  6931. var measureCharMinor = document.createElement('DIV');
  6932. measureCharMinor.className = 'text minor measure';
  6933. measureCharMinor.appendChild(text);
  6934. this.frame.appendChild(measureCharMinor);
  6935. dom.measureCharMinor = measureCharMinor;
  6936. }
  6937. if (!dom.measureCharMajor) {
  6938. text = document.createTextNode('0');
  6939. var measureCharMajor = document.createElement('DIV');
  6940. measureCharMajor.className = 'text major measure';
  6941. measureCharMajor.appendChild(text);
  6942. this.frame.appendChild(measureCharMajor);
  6943. dom.measureCharMajor = measureCharMajor;
  6944. }
  6945. };
  6946. /**
  6947. * Reflow the component
  6948. * @return {Boolean} resized
  6949. */
  6950. TimeAxis.prototype.reflow = function () {
  6951. var changed = 0,
  6952. update = util.updateProperty,
  6953. frame = this.frame,
  6954. range = this.range;
  6955. if (!range) {
  6956. throw new Error('Cannot repaint time axis: no range configured');
  6957. }
  6958. if (frame) {
  6959. changed += update(this, 'top', frame.offsetTop);
  6960. changed += update(this, 'left', frame.offsetLeft);
  6961. // calculate size of a character
  6962. var props = this.props,
  6963. showMinorLabels = this.getOption('showMinorLabels'),
  6964. showMajorLabels = this.getOption('showMajorLabels'),
  6965. measureCharMinor = this.dom.measureCharMinor,
  6966. measureCharMajor = this.dom.measureCharMajor;
  6967. if (measureCharMinor) {
  6968. props.minorCharHeight = measureCharMinor.clientHeight;
  6969. props.minorCharWidth = measureCharMinor.clientWidth;
  6970. }
  6971. if (measureCharMajor) {
  6972. props.majorCharHeight = measureCharMajor.clientHeight;
  6973. props.majorCharWidth = measureCharMajor.clientWidth;
  6974. }
  6975. var parentHeight = frame.parentNode ? frame.parentNode.offsetHeight : 0;
  6976. if (parentHeight != props.parentHeight) {
  6977. props.parentHeight = parentHeight;
  6978. changed += 1;
  6979. }
  6980. switch (this.getOption('orientation')) {
  6981. case 'bottom':
  6982. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  6983. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  6984. props.minorLabelTop = 0;
  6985. props.majorLabelTop = props.minorLabelTop + props.minorLabelHeight;
  6986. props.minorLineTop = -this.top;
  6987. props.minorLineHeight = Math.max(this.top + props.majorLabelHeight, 0);
  6988. props.minorLineWidth = 1; // TODO: really calculate width
  6989. props.majorLineTop = -this.top;
  6990. props.majorLineHeight = Math.max(this.top + props.minorLabelHeight + props.majorLabelHeight, 0);
  6991. props.majorLineWidth = 1; // TODO: really calculate width
  6992. props.lineTop = 0;
  6993. break;
  6994. case 'top':
  6995. props.minorLabelHeight = showMinorLabels ? props.minorCharHeight : 0;
  6996. props.majorLabelHeight = showMajorLabels ? props.majorCharHeight : 0;
  6997. props.majorLabelTop = 0;
  6998. props.minorLabelTop = props.majorLabelTop + props.majorLabelHeight;
  6999. props.minorLineTop = props.minorLabelTop;
  7000. props.minorLineHeight = Math.max(parentHeight - props.majorLabelHeight - this.top);
  7001. props.minorLineWidth = 1; // TODO: really calculate width
  7002. props.majorLineTop = 0;
  7003. props.majorLineHeight = Math.max(parentHeight - this.top);
  7004. props.majorLineWidth = 1; // TODO: really calculate width
  7005. props.lineTop = props.majorLabelHeight + props.minorLabelHeight;
  7006. break;
  7007. default:
  7008. throw new Error('Unkown orientation "' + this.getOption('orientation') + '"');
  7009. }
  7010. var height = props.minorLabelHeight + props.majorLabelHeight;
  7011. changed += update(this, 'width', frame.offsetWidth);
  7012. changed += update(this, 'height', height);
  7013. // calculate range and step
  7014. this._updateConversion();
  7015. var start = util.convert(range.start, 'Date'),
  7016. end = util.convert(range.end, 'Date'),
  7017. minimumStep = this.toTime((props.minorCharWidth || 10) * 5) - this.toTime(0);
  7018. this.step = new TimeStep(start, end, minimumStep);
  7019. changed += update(props.range, 'start', start.valueOf());
  7020. changed += update(props.range, 'end', end.valueOf());
  7021. changed += update(props.range, 'minimumStep', minimumStep.valueOf());
  7022. }
  7023. return (changed > 0);
  7024. };
  7025. /**
  7026. * Calculate the factor and offset to convert a position on screen to the
  7027. * corresponding date and vice versa.
  7028. * After the method _updateConversion is executed once, the methods toTime
  7029. * and toScreen can be used.
  7030. * @private
  7031. */
  7032. TimeAxis.prototype._updateConversion = function() {
  7033. var range = this.range;
  7034. if (!range) {
  7035. throw new Error('No range configured');
  7036. }
  7037. if (range.conversion) {
  7038. this.conversion = range.conversion(this.width);
  7039. }
  7040. else {
  7041. this.conversion = Range.conversion(range.start, range.end, this.width);
  7042. }
  7043. };
  7044. /**
  7045. * An ItemSet holds a set of items and ranges which can be displayed in a
  7046. * range. The width is determined by the parent of the ItemSet, and the height
  7047. * is determined by the size of the items.
  7048. * @param {Component} parent
  7049. * @param {Component[]} [depends] Components on which this components depends
  7050. * (except for the parent)
  7051. * @param {Object} [options] See ItemSet.setOptions for the available
  7052. * options.
  7053. * @constructor ItemSet
  7054. * @extends Panel
  7055. */
  7056. // TODO: improve performance by replacing all Array.forEach with a for loop
  7057. function ItemSet(parent, depends, options) {
  7058. this.id = util.randomUUID();
  7059. this.parent = parent;
  7060. this.depends = depends;
  7061. // one options object is shared by this itemset and all its items
  7062. this.options = options || {};
  7063. this.defaultOptions = {
  7064. type: 'box',
  7065. align: 'center',
  7066. orientation: 'bottom',
  7067. margin: {
  7068. axis: 20,
  7069. item: 10
  7070. },
  7071. padding: 5
  7072. };
  7073. this.dom = {};
  7074. var me = this;
  7075. this.itemsData = null; // DataSet
  7076. this.range = null; // Range or Object {start: number, end: number}
  7077. this.listeners = {
  7078. 'add': function (event, params, senderId) {
  7079. if (senderId != me.id) {
  7080. me._onAdd(params.items);
  7081. }
  7082. },
  7083. 'update': function (event, params, senderId) {
  7084. if (senderId != me.id) {
  7085. me._onUpdate(params.items);
  7086. }
  7087. },
  7088. 'remove': function (event, params, senderId) {
  7089. if (senderId != me.id) {
  7090. me._onRemove(params.items);
  7091. }
  7092. }
  7093. };
  7094. this.items = {}; // object with an Item for every data item
  7095. this.queue = {}; // queue with id/actions: 'add', 'update', 'delete'
  7096. this.stack = new Stack(this, Object.create(this.options));
  7097. this.conversion = null;
  7098. // TODO: ItemSet should also attach event listeners for rangechange and rangechanged, like timeaxis
  7099. }
  7100. ItemSet.prototype = new Panel();
  7101. // available item types will be registered here
  7102. ItemSet.types = {
  7103. box: ItemBox,
  7104. range: ItemRange,
  7105. point: ItemPoint
  7106. };
  7107. /**
  7108. * Set options for the ItemSet. Existing options will be extended/overwritten.
  7109. * @param {Object} [options] The following options are available:
  7110. * {String | function} [className]
  7111. * class name for the itemset
  7112. * {String} [type]
  7113. * Default type for the items. Choose from 'box'
  7114. * (default), 'point', or 'range'. The default
  7115. * Style can be overwritten by individual items.
  7116. * {String} align
  7117. * Alignment for the items, only applicable for
  7118. * ItemBox. Choose 'center' (default), 'left', or
  7119. * 'right'.
  7120. * {String} orientation
  7121. * Orientation of the item set. Choose 'top' or
  7122. * 'bottom' (default).
  7123. * {Number} margin.axis
  7124. * Margin between the axis and the items in pixels.
  7125. * Default is 20.
  7126. * {Number} margin.item
  7127. * Margin between items in pixels. Default is 10.
  7128. * {Number} padding
  7129. * Padding of the contents of an item in pixels.
  7130. * Must correspond with the items css. Default is 5.
  7131. */
  7132. ItemSet.prototype.setOptions = Component.prototype.setOptions;
  7133. /**
  7134. * Set range (start and end).
  7135. * @param {Range | Object} range A Range or an object containing start and end.
  7136. */
  7137. ItemSet.prototype.setRange = function setRange(range) {
  7138. if (!(range instanceof Range) && (!range || !range.start || !range.end)) {
  7139. throw new TypeError('Range must be an instance of Range, ' +
  7140. 'or an object containing start and end.');
  7141. }
  7142. this.range = range;
  7143. };
  7144. /**
  7145. * Repaint the component
  7146. * @return {Boolean} changed
  7147. */
  7148. ItemSet.prototype.repaint = function repaint() {
  7149. var changed = 0,
  7150. update = util.updateProperty,
  7151. asSize = util.option.asSize,
  7152. options = this.options,
  7153. orientation = this.getOption('orientation'),
  7154. defaultOptions = this.defaultOptions,
  7155. frame = this.frame;
  7156. if (!frame) {
  7157. frame = document.createElement('div');
  7158. frame.className = 'itemset';
  7159. var className = options.className;
  7160. if (className) {
  7161. util.addClassName(frame, util.option.asString(className));
  7162. }
  7163. // create background panel
  7164. var background = document.createElement('div');
  7165. background.className = 'background';
  7166. frame.appendChild(background);
  7167. this.dom.background = background;
  7168. // create foreground panel
  7169. var foreground = document.createElement('div');
  7170. foreground.className = 'foreground';
  7171. frame.appendChild(foreground);
  7172. this.dom.foreground = foreground;
  7173. // create axis panel
  7174. var axis = document.createElement('div');
  7175. axis.className = 'itemset-axis';
  7176. //frame.appendChild(axis);
  7177. this.dom.axis = axis;
  7178. this.frame = frame;
  7179. changed += 1;
  7180. }
  7181. if (!this.parent) {
  7182. throw new Error('Cannot repaint itemset: no parent attached');
  7183. }
  7184. var parentContainer = this.parent.getContainer();
  7185. if (!parentContainer) {
  7186. throw new Error('Cannot repaint itemset: parent has no container element');
  7187. }
  7188. if (!frame.parentNode) {
  7189. parentContainer.appendChild(frame);
  7190. changed += 1;
  7191. }
  7192. if (!this.dom.axis.parentNode) {
  7193. parentContainer.appendChild(this.dom.axis);
  7194. changed += 1;
  7195. }
  7196. // reposition frame
  7197. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  7198. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  7199. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  7200. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  7201. // reposition axis
  7202. changed += update(this.dom.axis.style, 'left', asSize(options.left, '0px'));
  7203. changed += update(this.dom.axis.style, 'width', asSize(options.width, '100%'));
  7204. if (orientation == 'bottom') {
  7205. changed += update(this.dom.axis.style, 'top', (this.height + this.top) + 'px');
  7206. }
  7207. else { // orientation == 'top'
  7208. changed += update(this.dom.axis.style, 'top', this.top + 'px');
  7209. }
  7210. this._updateConversion();
  7211. var me = this,
  7212. queue = this.queue,
  7213. itemsData = this.itemsData,
  7214. items = this.items,
  7215. dataOptions = {
  7216. // TODO: cleanup
  7217. // fields: [(itemsData && itemsData.fieldId || 'id'), 'start', 'end', 'content', 'type', 'className']
  7218. };
  7219. // show/hide added/changed/removed items
  7220. Object.keys(queue).forEach(function (id) {
  7221. //var entry = queue[id];
  7222. var action = queue[id];
  7223. var item = items[id];
  7224. //var item = entry.item;
  7225. //noinspection FallthroughInSwitchStatementJS
  7226. switch (action) {
  7227. case 'add':
  7228. case 'update':
  7229. var itemData = itemsData && itemsData.get(id, dataOptions);
  7230. if (itemData) {
  7231. var type = itemData.type ||
  7232. (itemData.start && itemData.end && 'range') ||
  7233. options.type ||
  7234. 'box';
  7235. var constructor = ItemSet.types[type];
  7236. // TODO: how to handle items with invalid data? hide them and give a warning? or throw an error?
  7237. if (item) {
  7238. // update item
  7239. if (!constructor || !(item instanceof constructor)) {
  7240. // item type has changed, hide and delete the item
  7241. changed += item.hide();
  7242. item = null;
  7243. }
  7244. else {
  7245. item.data = itemData; // TODO: create a method item.setData ?
  7246. changed++;
  7247. }
  7248. }
  7249. if (!item) {
  7250. // create item
  7251. if (constructor) {
  7252. item = new constructor(me, itemData, options, defaultOptions);
  7253. changed++;
  7254. }
  7255. else {
  7256. throw new TypeError('Unknown item type "' + type + '"');
  7257. }
  7258. }
  7259. // force a repaint (not only a reposition)
  7260. item.repaint();
  7261. items[id] = item;
  7262. }
  7263. // update queue
  7264. delete queue[id];
  7265. break;
  7266. case 'remove':
  7267. if (item) {
  7268. // remove DOM of the item
  7269. changed += item.hide();
  7270. }
  7271. // update lists
  7272. delete items[id];
  7273. delete queue[id];
  7274. break;
  7275. default:
  7276. console.log('Error: unknown action "' + action + '"');
  7277. }
  7278. });
  7279. // reposition all items. Show items only when in the visible area
  7280. util.forEach(this.items, function (item) {
  7281. if (item.visible) {
  7282. changed += item.show();
  7283. item.reposition();
  7284. }
  7285. else {
  7286. changed += item.hide();
  7287. }
  7288. });
  7289. return (changed > 0);
  7290. };
  7291. /**
  7292. * Get the foreground container element
  7293. * @return {HTMLElement} foreground
  7294. */
  7295. ItemSet.prototype.getForeground = function getForeground() {
  7296. return this.dom.foreground;
  7297. };
  7298. /**
  7299. * Get the background container element
  7300. * @return {HTMLElement} background
  7301. */
  7302. ItemSet.prototype.getBackground = function getBackground() {
  7303. return this.dom.background;
  7304. };
  7305. /**
  7306. * Get the axis container element
  7307. * @return {HTMLElement} axis
  7308. */
  7309. ItemSet.prototype.getAxis = function getAxis() {
  7310. return this.dom.axis;
  7311. };
  7312. /**
  7313. * Reflow the component
  7314. * @return {Boolean} resized
  7315. */
  7316. ItemSet.prototype.reflow = function reflow () {
  7317. var changed = 0,
  7318. options = this.options,
  7319. marginAxis = options.margin && options.margin.axis || this.defaultOptions.margin.axis,
  7320. marginItem = options.margin && options.margin.item || this.defaultOptions.margin.item,
  7321. update = util.updateProperty,
  7322. asNumber = util.option.asNumber,
  7323. asSize = util.option.asSize,
  7324. frame = this.frame;
  7325. if (frame) {
  7326. this._updateConversion();
  7327. util.forEach(this.items, function (item) {
  7328. changed += item.reflow();
  7329. });
  7330. // TODO: stack.update should be triggered via an event, in stack itself
  7331. // TODO: only update the stack when there are changed items
  7332. this.stack.update();
  7333. var maxHeight = asNumber(options.maxHeight);
  7334. var fixedHeight = (asSize(options.height) != null);
  7335. var height;
  7336. if (fixedHeight) {
  7337. height = frame.offsetHeight;
  7338. }
  7339. else {
  7340. // height is not specified, determine the height from the height and positioned items
  7341. var visibleItems = this.stack.ordered; // TODO: not so nice way to get the filtered items
  7342. if (visibleItems.length) {
  7343. var min = visibleItems[0].top;
  7344. var max = visibleItems[0].top + visibleItems[0].height;
  7345. util.forEach(visibleItems, function (item) {
  7346. min = Math.min(min, item.top);
  7347. max = Math.max(max, (item.top + item.height));
  7348. });
  7349. height = (max - min) + marginAxis + marginItem;
  7350. }
  7351. else {
  7352. height = marginAxis + marginItem;
  7353. }
  7354. }
  7355. if (maxHeight != null) {
  7356. height = Math.min(height, maxHeight);
  7357. }
  7358. changed += update(this, 'height', height);
  7359. // calculate height from items
  7360. changed += update(this, 'top', frame.offsetTop);
  7361. changed += update(this, 'left', frame.offsetLeft);
  7362. changed += update(this, 'width', frame.offsetWidth);
  7363. }
  7364. else {
  7365. changed += 1;
  7366. }
  7367. return (changed > 0);
  7368. };
  7369. /**
  7370. * Hide this component from the DOM
  7371. * @return {Boolean} changed
  7372. */
  7373. ItemSet.prototype.hide = function hide() {
  7374. var changed = false;
  7375. // remove the DOM
  7376. if (this.frame && this.frame.parentNode) {
  7377. this.frame.parentNode.removeChild(this.frame);
  7378. changed = true;
  7379. }
  7380. if (this.dom.axis && this.dom.axis.parentNode) {
  7381. this.dom.axis.parentNode.removeChild(this.dom.axis);
  7382. changed = true;
  7383. }
  7384. return changed;
  7385. };
  7386. /**
  7387. * Set items
  7388. * @param {vis.DataSet | null} items
  7389. */
  7390. ItemSet.prototype.setItems = function setItems(items) {
  7391. var me = this,
  7392. ids,
  7393. oldItemsData = this.itemsData;
  7394. // replace the dataset
  7395. if (!items) {
  7396. this.itemsData = null;
  7397. }
  7398. else if (items instanceof DataSet || items instanceof DataView) {
  7399. this.itemsData = items;
  7400. }
  7401. else {
  7402. throw new TypeError('Data must be an instance of DataSet');
  7403. }
  7404. if (oldItemsData) {
  7405. // unsubscribe from old dataset
  7406. util.forEach(this.listeners, function (callback, event) {
  7407. oldItemsData.unsubscribe(event, callback);
  7408. });
  7409. // remove all drawn items
  7410. ids = oldItemsData.getIds();
  7411. this._onRemove(ids);
  7412. }
  7413. if (this.itemsData) {
  7414. // subscribe to new dataset
  7415. var id = this.id;
  7416. util.forEach(this.listeners, function (callback, event) {
  7417. me.itemsData.subscribe(event, callback, id);
  7418. });
  7419. // draw all new items
  7420. ids = this.itemsData.getIds();
  7421. this._onAdd(ids);
  7422. }
  7423. };
  7424. /**
  7425. * Get the current items items
  7426. * @returns {vis.DataSet | null}
  7427. */
  7428. ItemSet.prototype.getItems = function getItems() {
  7429. return this.itemsData;
  7430. };
  7431. /**
  7432. * Handle updated items
  7433. * @param {Number[]} ids
  7434. * @private
  7435. */
  7436. ItemSet.prototype._onUpdate = function _onUpdate(ids) {
  7437. this._toQueue('update', ids);
  7438. };
  7439. /**
  7440. * Handle changed items
  7441. * @param {Number[]} ids
  7442. * @private
  7443. */
  7444. ItemSet.prototype._onAdd = function _onAdd(ids) {
  7445. this._toQueue('add', ids);
  7446. };
  7447. /**
  7448. * Handle removed items
  7449. * @param {Number[]} ids
  7450. * @private
  7451. */
  7452. ItemSet.prototype._onRemove = function _onRemove(ids) {
  7453. this._toQueue('remove', ids);
  7454. };
  7455. /**
  7456. * Put items in the queue to be added/updated/remove
  7457. * @param {String} action can be 'add', 'update', 'remove'
  7458. * @param {Number[]} ids
  7459. */
  7460. ItemSet.prototype._toQueue = function _toQueue(action, ids) {
  7461. var queue = this.queue;
  7462. ids.forEach(function (id) {
  7463. queue[id] = action;
  7464. });
  7465. if (this.controller) {
  7466. //this.requestReflow();
  7467. this.requestRepaint();
  7468. }
  7469. };
  7470. /**
  7471. * Calculate the factor and offset to convert a position on screen to the
  7472. * corresponding date and vice versa.
  7473. * After the method _updateConversion is executed once, the methods toTime
  7474. * and toScreen can be used.
  7475. * @private
  7476. */
  7477. ItemSet.prototype._updateConversion = function _updateConversion() {
  7478. var range = this.range;
  7479. if (!range) {
  7480. throw new Error('No range configured');
  7481. }
  7482. if (range.conversion) {
  7483. this.conversion = range.conversion(this.width);
  7484. }
  7485. else {
  7486. this.conversion = Range.conversion(range.start, range.end, this.width);
  7487. }
  7488. };
  7489. /**
  7490. * Convert a position on screen (pixels) to a datetime
  7491. * Before this method can be used, the method _updateConversion must be
  7492. * executed once.
  7493. * @param {int} x Position on the screen in pixels
  7494. * @return {Date} time The datetime the corresponds with given position x
  7495. */
  7496. ItemSet.prototype.toTime = function toTime(x) {
  7497. var conversion = this.conversion;
  7498. return new Date(x / conversion.factor + conversion.offset);
  7499. };
  7500. /**
  7501. * Convert a datetime (Date object) into a position on the screen
  7502. * Before this method can be used, the method _updateConversion must be
  7503. * executed once.
  7504. * @param {Date} time A date
  7505. * @return {int} x The position on the screen in pixels which corresponds
  7506. * with the given date.
  7507. */
  7508. ItemSet.prototype.toScreen = function toScreen(time) {
  7509. var conversion = this.conversion;
  7510. return (time.valueOf() - conversion.offset) * conversion.factor;
  7511. };
  7512. /**
  7513. * @constructor Item
  7514. * @param {ItemSet} parent
  7515. * @param {Object} data Object containing (optional) parameters type,
  7516. * start, end, content, group, className.
  7517. * @param {Object} [options] Options to set initial property values
  7518. * @param {Object} [defaultOptions] default options
  7519. * // TODO: describe available options
  7520. */
  7521. function Item (parent, data, options, defaultOptions) {
  7522. this.parent = parent;
  7523. this.data = data;
  7524. this.dom = null;
  7525. this.options = options || {};
  7526. this.defaultOptions = defaultOptions || {};
  7527. this.selected = false;
  7528. this.visible = false;
  7529. this.top = 0;
  7530. this.left = 0;
  7531. this.width = 0;
  7532. this.height = 0;
  7533. }
  7534. /**
  7535. * Select current item
  7536. */
  7537. Item.prototype.select = function select() {
  7538. this.selected = true;
  7539. };
  7540. /**
  7541. * Unselect current item
  7542. */
  7543. Item.prototype.unselect = function unselect() {
  7544. this.selected = false;
  7545. };
  7546. /**
  7547. * Show the Item in the DOM (when not already visible)
  7548. * @return {Boolean} changed
  7549. */
  7550. Item.prototype.show = function show() {
  7551. return false;
  7552. };
  7553. /**
  7554. * Hide the Item from the DOM (when visible)
  7555. * @return {Boolean} changed
  7556. */
  7557. Item.prototype.hide = function hide() {
  7558. return false;
  7559. };
  7560. /**
  7561. * Repaint the item
  7562. * @return {Boolean} changed
  7563. */
  7564. Item.prototype.repaint = function repaint() {
  7565. // should be implemented by the item
  7566. return false;
  7567. };
  7568. /**
  7569. * Reflow the item
  7570. * @return {Boolean} resized
  7571. */
  7572. Item.prototype.reflow = function reflow() {
  7573. // should be implemented by the item
  7574. return false;
  7575. };
  7576. /**
  7577. * @constructor ItemBox
  7578. * @extends Item
  7579. * @param {ItemSet} parent
  7580. * @param {Object} data Object containing parameters start
  7581. * content, className.
  7582. * @param {Object} [options] Options to set initial property values
  7583. * @param {Object} [defaultOptions] default options
  7584. * // TODO: describe available options
  7585. */
  7586. function ItemBox (parent, data, options, defaultOptions) {
  7587. this.props = {
  7588. dot: {
  7589. left: 0,
  7590. top: 0,
  7591. width: 0,
  7592. height: 0
  7593. },
  7594. line: {
  7595. top: 0,
  7596. left: 0,
  7597. width: 0,
  7598. height: 0
  7599. }
  7600. };
  7601. Item.call(this, parent, data, options, defaultOptions);
  7602. }
  7603. ItemBox.prototype = new Item (null, null);
  7604. /**
  7605. * Select the item
  7606. * @override
  7607. */
  7608. ItemBox.prototype.select = function select() {
  7609. this.selected = true;
  7610. // TODO: select and unselect
  7611. };
  7612. /**
  7613. * Unselect the item
  7614. * @override
  7615. */
  7616. ItemBox.prototype.unselect = function unselect() {
  7617. this.selected = false;
  7618. // TODO: select and unselect
  7619. };
  7620. /**
  7621. * Repaint the item
  7622. * @return {Boolean} changed
  7623. */
  7624. ItemBox.prototype.repaint = function repaint() {
  7625. // TODO: make an efficient repaint
  7626. var changed = false;
  7627. var dom = this.dom;
  7628. if (!dom) {
  7629. this._create();
  7630. dom = this.dom;
  7631. changed = true;
  7632. }
  7633. if (dom) {
  7634. if (!this.parent) {
  7635. throw new Error('Cannot repaint item: no parent attached');
  7636. }
  7637. var foreground = this.parent.getForeground();
  7638. if (!foreground) {
  7639. throw new Error('Cannot repaint time axis: ' +
  7640. 'parent has no foreground container element');
  7641. }
  7642. var background = this.parent.getBackground();
  7643. if (!background) {
  7644. throw new Error('Cannot repaint time axis: ' +
  7645. 'parent has no background container element');
  7646. }
  7647. var axis = this.parent.getAxis();
  7648. if (!background) {
  7649. throw new Error('Cannot repaint time axis: ' +
  7650. 'parent has no axis container element');
  7651. }
  7652. if (!dom.box.parentNode) {
  7653. foreground.appendChild(dom.box);
  7654. changed = true;
  7655. }
  7656. if (!dom.line.parentNode) {
  7657. background.appendChild(dom.line);
  7658. changed = true;
  7659. }
  7660. if (!dom.dot.parentNode) {
  7661. axis.appendChild(dom.dot);
  7662. changed = true;
  7663. }
  7664. // update contents
  7665. if (this.data.content != this.content) {
  7666. this.content = this.data.content;
  7667. if (this.content instanceof Element) {
  7668. dom.content.innerHTML = '';
  7669. dom.content.appendChild(this.content);
  7670. }
  7671. else if (this.data.content != undefined) {
  7672. dom.content.innerHTML = this.content;
  7673. }
  7674. else {
  7675. throw new Error('Property "content" missing in item ' + this.data.id);
  7676. }
  7677. changed = true;
  7678. }
  7679. // update class
  7680. var className = (this.data.className? ' ' + this.data.className : '') +
  7681. (this.selected ? ' selected' : '');
  7682. if (this.className != className) {
  7683. this.className = className;
  7684. dom.box.className = 'item box' + className;
  7685. dom.line.className = 'item line' + className;
  7686. dom.dot.className = 'item dot' + className;
  7687. changed = true;
  7688. }
  7689. }
  7690. return changed;
  7691. };
  7692. /**
  7693. * Show the item in the DOM (when not already visible). The items DOM will
  7694. * be created when needed.
  7695. * @return {Boolean} changed
  7696. */
  7697. ItemBox.prototype.show = function show() {
  7698. if (!this.dom || !this.dom.box.parentNode) {
  7699. return this.repaint();
  7700. }
  7701. else {
  7702. return false;
  7703. }
  7704. };
  7705. /**
  7706. * Hide the item from the DOM (when visible)
  7707. * @return {Boolean} changed
  7708. */
  7709. ItemBox.prototype.hide = function hide() {
  7710. var changed = false,
  7711. dom = this.dom;
  7712. if (dom) {
  7713. if (dom.box.parentNode) {
  7714. dom.box.parentNode.removeChild(dom.box);
  7715. changed = true;
  7716. }
  7717. if (dom.line.parentNode) {
  7718. dom.line.parentNode.removeChild(dom.line);
  7719. }
  7720. if (dom.dot.parentNode) {
  7721. dom.dot.parentNode.removeChild(dom.dot);
  7722. }
  7723. }
  7724. return changed;
  7725. };
  7726. /**
  7727. * Reflow the item: calculate its actual size and position from the DOM
  7728. * @return {boolean} resized returns true if the axis is resized
  7729. * @override
  7730. */
  7731. ItemBox.prototype.reflow = function reflow() {
  7732. var changed = 0,
  7733. update,
  7734. dom,
  7735. props,
  7736. options,
  7737. margin,
  7738. start,
  7739. align,
  7740. orientation,
  7741. top,
  7742. left,
  7743. data,
  7744. range;
  7745. if (this.data.start == undefined) {
  7746. throw new Error('Property "start" missing in item ' + this.data.id);
  7747. }
  7748. data = this.data;
  7749. range = this.parent && this.parent.range;
  7750. if (data && range) {
  7751. // TODO: account for the width of the item
  7752. var interval = (range.end - range.start);
  7753. this.visible = (data.start > range.start - interval) && (data.start < range.end + interval);
  7754. }
  7755. else {
  7756. this.visible = false;
  7757. }
  7758. if (this.visible) {
  7759. dom = this.dom;
  7760. if (dom) {
  7761. update = util.updateProperty;
  7762. props = this.props;
  7763. options = this.options;
  7764. start = this.parent.toScreen(this.data.start);
  7765. align = options.align || this.defaultOptions.align;
  7766. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  7767. orientation = options.orientation || this.defaultOptions.orientation;
  7768. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  7769. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  7770. changed += update(props.line, 'width', dom.line.offsetWidth);
  7771. changed += update(props.line, 'height', dom.line.offsetHeight);
  7772. changed += update(props.line, 'top', dom.line.offsetTop);
  7773. changed += update(this, 'width', dom.box.offsetWidth);
  7774. changed += update(this, 'height', dom.box.offsetHeight);
  7775. if (align == 'right') {
  7776. left = start - this.width;
  7777. }
  7778. else if (align == 'left') {
  7779. left = start;
  7780. }
  7781. else {
  7782. // default or 'center'
  7783. left = start - this.width / 2;
  7784. }
  7785. changed += update(this, 'left', left);
  7786. changed += update(props.line, 'left', start - props.line.width / 2);
  7787. changed += update(props.dot, 'left', start - props.dot.width / 2);
  7788. changed += update(props.dot, 'top', -props.dot.height / 2);
  7789. if (orientation == 'top') {
  7790. top = margin;
  7791. changed += update(this, 'top', top);
  7792. }
  7793. else {
  7794. // default or 'bottom'
  7795. var parentHeight = this.parent.height;
  7796. top = parentHeight - this.height - margin;
  7797. changed += update(this, 'top', top);
  7798. }
  7799. }
  7800. else {
  7801. changed += 1;
  7802. }
  7803. }
  7804. return (changed > 0);
  7805. };
  7806. /**
  7807. * Create an items DOM
  7808. * @private
  7809. */
  7810. ItemBox.prototype._create = function _create() {
  7811. var dom = this.dom;
  7812. if (!dom) {
  7813. this.dom = dom = {};
  7814. // create the box
  7815. dom.box = document.createElement('DIV');
  7816. // className is updated in repaint()
  7817. // contents box (inside the background box). used for making margins
  7818. dom.content = document.createElement('DIV');
  7819. dom.content.className = 'content';
  7820. dom.box.appendChild(dom.content);
  7821. // line to axis
  7822. dom.line = document.createElement('DIV');
  7823. dom.line.className = 'line';
  7824. // dot on axis
  7825. dom.dot = document.createElement('DIV');
  7826. dom.dot.className = 'dot';
  7827. }
  7828. };
  7829. /**
  7830. * Reposition the item, recalculate its left, top, and width, using the current
  7831. * range and size of the items itemset
  7832. * @override
  7833. */
  7834. ItemBox.prototype.reposition = function reposition() {
  7835. var dom = this.dom,
  7836. props = this.props,
  7837. orientation = this.options.orientation || this.defaultOptions.orientation;
  7838. if (dom) {
  7839. var box = dom.box,
  7840. line = dom.line,
  7841. dot = dom.dot;
  7842. box.style.left = this.left + 'px';
  7843. box.style.top = this.top + 'px';
  7844. line.style.left = props.line.left + 'px';
  7845. if (orientation == 'top') {
  7846. line.style.top = 0 + 'px';
  7847. line.style.height = this.top + 'px';
  7848. }
  7849. else {
  7850. // orientation 'bottom'
  7851. line.style.top = (this.top + this.height) + 'px';
  7852. line.style.height = Math.max(this.parent.height - this.top - this.height +
  7853. this.props.dot.height / 2, 0) + 'px';
  7854. }
  7855. dot.style.left = props.dot.left + 'px';
  7856. dot.style.top = props.dot.top + 'px';
  7857. }
  7858. };
  7859. /**
  7860. * @constructor ItemPoint
  7861. * @extends Item
  7862. * @param {ItemSet} parent
  7863. * @param {Object} data Object containing parameters start
  7864. * content, className.
  7865. * @param {Object} [options] Options to set initial property values
  7866. * @param {Object} [defaultOptions] default options
  7867. * // TODO: describe available options
  7868. */
  7869. function ItemPoint (parent, data, options, defaultOptions) {
  7870. this.props = {
  7871. dot: {
  7872. top: 0,
  7873. width: 0,
  7874. height: 0
  7875. },
  7876. content: {
  7877. height: 0,
  7878. marginLeft: 0
  7879. }
  7880. };
  7881. Item.call(this, parent, data, options, defaultOptions);
  7882. }
  7883. ItemPoint.prototype = new Item (null, null);
  7884. /**
  7885. * Select the item
  7886. * @override
  7887. */
  7888. ItemPoint.prototype.select = function select() {
  7889. this.selected = true;
  7890. // TODO: select and unselect
  7891. };
  7892. /**
  7893. * Unselect the item
  7894. * @override
  7895. */
  7896. ItemPoint.prototype.unselect = function unselect() {
  7897. this.selected = false;
  7898. // TODO: select and unselect
  7899. };
  7900. /**
  7901. * Repaint the item
  7902. * @return {Boolean} changed
  7903. */
  7904. ItemPoint.prototype.repaint = function repaint() {
  7905. // TODO: make an efficient repaint
  7906. var changed = false;
  7907. var dom = this.dom;
  7908. if (!dom) {
  7909. this._create();
  7910. dom = this.dom;
  7911. changed = true;
  7912. }
  7913. if (dom) {
  7914. if (!this.parent) {
  7915. throw new Error('Cannot repaint item: no parent attached');
  7916. }
  7917. var foreground = this.parent.getForeground();
  7918. if (!foreground) {
  7919. throw new Error('Cannot repaint time axis: ' +
  7920. 'parent has no foreground container element');
  7921. }
  7922. if (!dom.point.parentNode) {
  7923. foreground.appendChild(dom.point);
  7924. foreground.appendChild(dom.point);
  7925. changed = true;
  7926. }
  7927. // update contents
  7928. if (this.data.content != this.content) {
  7929. this.content = this.data.content;
  7930. if (this.content instanceof Element) {
  7931. dom.content.innerHTML = '';
  7932. dom.content.appendChild(this.content);
  7933. }
  7934. else if (this.data.content != undefined) {
  7935. dom.content.innerHTML = this.content;
  7936. }
  7937. else {
  7938. throw new Error('Property "content" missing in item ' + this.data.id);
  7939. }
  7940. changed = true;
  7941. }
  7942. // update class
  7943. var className = (this.data.className? ' ' + this.data.className : '') +
  7944. (this.selected ? ' selected' : '');
  7945. if (this.className != className) {
  7946. this.className = className;
  7947. dom.point.className = 'item point' + className;
  7948. changed = true;
  7949. }
  7950. }
  7951. return changed;
  7952. };
  7953. /**
  7954. * Show the item in the DOM (when not already visible). The items DOM will
  7955. * be created when needed.
  7956. * @return {Boolean} changed
  7957. */
  7958. ItemPoint.prototype.show = function show() {
  7959. if (!this.dom || !this.dom.point.parentNode) {
  7960. return this.repaint();
  7961. }
  7962. else {
  7963. return false;
  7964. }
  7965. };
  7966. /**
  7967. * Hide the item from the DOM (when visible)
  7968. * @return {Boolean} changed
  7969. */
  7970. ItemPoint.prototype.hide = function hide() {
  7971. var changed = false,
  7972. dom = this.dom;
  7973. if (dom) {
  7974. if (dom.point.parentNode) {
  7975. dom.point.parentNode.removeChild(dom.point);
  7976. changed = true;
  7977. }
  7978. }
  7979. return changed;
  7980. };
  7981. /**
  7982. * Reflow the item: calculate its actual size from the DOM
  7983. * @return {boolean} resized returns true if the axis is resized
  7984. * @override
  7985. */
  7986. ItemPoint.prototype.reflow = function reflow() {
  7987. var changed = 0,
  7988. update,
  7989. dom,
  7990. props,
  7991. options,
  7992. margin,
  7993. orientation,
  7994. start,
  7995. top,
  7996. data,
  7997. range;
  7998. if (this.data.start == undefined) {
  7999. throw new Error('Property "start" missing in item ' + this.data.id);
  8000. }
  8001. data = this.data;
  8002. range = this.parent && this.parent.range;
  8003. if (data && range) {
  8004. // TODO: account for the width of the item
  8005. var interval = (range.end - range.start);
  8006. this.visible = (data.start > range.start - interval) && (data.start < range.end);
  8007. }
  8008. else {
  8009. this.visible = false;
  8010. }
  8011. if (this.visible) {
  8012. dom = this.dom;
  8013. if (dom) {
  8014. update = util.updateProperty;
  8015. props = this.props;
  8016. options = this.options;
  8017. orientation = options.orientation || this.defaultOptions.orientation;
  8018. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  8019. start = this.parent.toScreen(this.data.start);
  8020. changed += update(this, 'width', dom.point.offsetWidth);
  8021. changed += update(this, 'height', dom.point.offsetHeight);
  8022. changed += update(props.dot, 'width', dom.dot.offsetWidth);
  8023. changed += update(props.dot, 'height', dom.dot.offsetHeight);
  8024. changed += update(props.content, 'height', dom.content.offsetHeight);
  8025. if (orientation == 'top') {
  8026. top = margin;
  8027. }
  8028. else {
  8029. // default or 'bottom'
  8030. var parentHeight = this.parent.height;
  8031. top = Math.max(parentHeight - this.height - margin, 0);
  8032. }
  8033. changed += update(this, 'top', top);
  8034. changed += update(this, 'left', start - props.dot.width / 2);
  8035. changed += update(props.content, 'marginLeft', 1.5 * props.dot.width);
  8036. //changed += update(props.content, 'marginRight', 0.5 * props.dot.width); // TODO
  8037. changed += update(props.dot, 'top', (this.height - props.dot.height) / 2);
  8038. }
  8039. else {
  8040. changed += 1;
  8041. }
  8042. }
  8043. return (changed > 0);
  8044. };
  8045. /**
  8046. * Create an items DOM
  8047. * @private
  8048. */
  8049. ItemPoint.prototype._create = function _create() {
  8050. var dom = this.dom;
  8051. if (!dom) {
  8052. this.dom = dom = {};
  8053. // background box
  8054. dom.point = document.createElement('div');
  8055. // className is updated in repaint()
  8056. // contents box, right from the dot
  8057. dom.content = document.createElement('div');
  8058. dom.content.className = 'content';
  8059. dom.point.appendChild(dom.content);
  8060. // dot at start
  8061. dom.dot = document.createElement('div');
  8062. dom.dot.className = 'dot';
  8063. dom.point.appendChild(dom.dot);
  8064. }
  8065. };
  8066. /**
  8067. * Reposition the item, recalculate its left, top, and width, using the current
  8068. * range and size of the items itemset
  8069. * @override
  8070. */
  8071. ItemPoint.prototype.reposition = function reposition() {
  8072. var dom = this.dom,
  8073. props = this.props;
  8074. if (dom) {
  8075. dom.point.style.top = this.top + 'px';
  8076. dom.point.style.left = this.left + 'px';
  8077. dom.content.style.marginLeft = props.content.marginLeft + 'px';
  8078. //dom.content.style.marginRight = props.content.marginRight + 'px'; // TODO
  8079. dom.dot.style.top = props.dot.top + 'px';
  8080. }
  8081. };
  8082. /**
  8083. * @constructor ItemRange
  8084. * @extends Item
  8085. * @param {ItemSet} parent
  8086. * @param {Object} data Object containing parameters start, end
  8087. * content, className.
  8088. * @param {Object} [options] Options to set initial property values
  8089. * @param {Object} [defaultOptions] default options
  8090. * // TODO: describe available options
  8091. */
  8092. function ItemRange (parent, data, options, defaultOptions) {
  8093. this.props = {
  8094. content: {
  8095. left: 0,
  8096. width: 0
  8097. }
  8098. };
  8099. Item.call(this, parent, data, options, defaultOptions);
  8100. }
  8101. ItemRange.prototype = new Item (null, null);
  8102. /**
  8103. * Select the item
  8104. * @override
  8105. */
  8106. ItemRange.prototype.select = function select() {
  8107. this.selected = true;
  8108. // TODO: select and unselect
  8109. };
  8110. /**
  8111. * Unselect the item
  8112. * @override
  8113. */
  8114. ItemRange.prototype.unselect = function unselect() {
  8115. this.selected = false;
  8116. // TODO: select and unselect
  8117. };
  8118. /**
  8119. * Repaint the item
  8120. * @return {Boolean} changed
  8121. */
  8122. ItemRange.prototype.repaint = function repaint() {
  8123. // TODO: make an efficient repaint
  8124. var changed = false;
  8125. var dom = this.dom;
  8126. if (!dom) {
  8127. this._create();
  8128. dom = this.dom;
  8129. changed = true;
  8130. }
  8131. if (dom) {
  8132. if (!this.parent) {
  8133. throw new Error('Cannot repaint item: no parent attached');
  8134. }
  8135. var foreground = this.parent.getForeground();
  8136. if (!foreground) {
  8137. throw new Error('Cannot repaint time axis: ' +
  8138. 'parent has no foreground container element');
  8139. }
  8140. if (!dom.box.parentNode) {
  8141. foreground.appendChild(dom.box);
  8142. changed = true;
  8143. }
  8144. // update content
  8145. if (this.data.content != this.content) {
  8146. this.content = this.data.content;
  8147. if (this.content instanceof Element) {
  8148. dom.content.innerHTML = '';
  8149. dom.content.appendChild(this.content);
  8150. }
  8151. else if (this.data.content != undefined) {
  8152. dom.content.innerHTML = this.content;
  8153. }
  8154. else {
  8155. throw new Error('Property "content" missing in item ' + this.data.id);
  8156. }
  8157. changed = true;
  8158. }
  8159. // update class
  8160. var className = this.data.className ? (' ' + this.data.className) : '';
  8161. if (this.className != className) {
  8162. this.className = className;
  8163. dom.box.className = 'item range' + className;
  8164. changed = true;
  8165. }
  8166. }
  8167. return changed;
  8168. };
  8169. /**
  8170. * Show the item in the DOM (when not already visible). The items DOM will
  8171. * be created when needed.
  8172. * @return {Boolean} changed
  8173. */
  8174. ItemRange.prototype.show = function show() {
  8175. if (!this.dom || !this.dom.box.parentNode) {
  8176. return this.repaint();
  8177. }
  8178. else {
  8179. return false;
  8180. }
  8181. };
  8182. /**
  8183. * Hide the item from the DOM (when visible)
  8184. * @return {Boolean} changed
  8185. */
  8186. ItemRange.prototype.hide = function hide() {
  8187. var changed = false,
  8188. dom = this.dom;
  8189. if (dom) {
  8190. if (dom.box.parentNode) {
  8191. dom.box.parentNode.removeChild(dom.box);
  8192. changed = true;
  8193. }
  8194. }
  8195. return changed;
  8196. };
  8197. /**
  8198. * Reflow the item: calculate its actual size from the DOM
  8199. * @return {boolean} resized returns true if the axis is resized
  8200. * @override
  8201. */
  8202. ItemRange.prototype.reflow = function reflow() {
  8203. var changed = 0,
  8204. dom,
  8205. props,
  8206. options,
  8207. margin,
  8208. padding,
  8209. parent,
  8210. start,
  8211. end,
  8212. data,
  8213. range,
  8214. update,
  8215. box,
  8216. parentWidth,
  8217. contentLeft,
  8218. orientation,
  8219. top;
  8220. if (this.data.start == undefined) {
  8221. throw new Error('Property "start" missing in item ' + this.data.id);
  8222. }
  8223. if (this.data.end == undefined) {
  8224. throw new Error('Property "end" missing in item ' + this.data.id);
  8225. }
  8226. data = this.data;
  8227. range = this.parent && this.parent.range;
  8228. if (data && range) {
  8229. // TODO: account for the width of the item. Take some margin
  8230. this.visible = (data.start < range.end) && (data.end > range.start);
  8231. }
  8232. else {
  8233. this.visible = false;
  8234. }
  8235. if (this.visible) {
  8236. dom = this.dom;
  8237. if (dom) {
  8238. props = this.props;
  8239. options = this.options;
  8240. parent = this.parent;
  8241. start = parent.toScreen(this.data.start);
  8242. end = parent.toScreen(this.data.end);
  8243. update = util.updateProperty;
  8244. box = dom.box;
  8245. parentWidth = parent.width;
  8246. orientation = options.orientation || this.defaultOptions.orientation;
  8247. margin = options.margin && options.margin.axis || this.defaultOptions.margin.axis;
  8248. padding = options.padding || this.defaultOptions.padding;
  8249. changed += update(props.content, 'width', dom.content.offsetWidth);
  8250. changed += update(this, 'height', box.offsetHeight);
  8251. // limit the width of the this, as browsers cannot draw very wide divs
  8252. if (start < -parentWidth) {
  8253. start = -parentWidth;
  8254. }
  8255. if (end > 2 * parentWidth) {
  8256. end = 2 * parentWidth;
  8257. }
  8258. // when range exceeds left of the window, position the contents at the left of the visible area
  8259. if (start < 0) {
  8260. contentLeft = Math.min(-start,
  8261. (end - start - props.content.width - 2 * padding));
  8262. // TODO: remove the need for options.padding. it's terrible.
  8263. }
  8264. else {
  8265. contentLeft = 0;
  8266. }
  8267. changed += update(props.content, 'left', contentLeft);
  8268. if (orientation == 'top') {
  8269. top = margin;
  8270. changed += update(this, 'top', top);
  8271. }
  8272. else {
  8273. // default or 'bottom'
  8274. top = parent.height - this.height - margin;
  8275. changed += update(this, 'top', top);
  8276. }
  8277. changed += update(this, 'left', start);
  8278. changed += update(this, 'width', Math.max(end - start, 1)); // TODO: reckon with border width;
  8279. }
  8280. else {
  8281. changed += 1;
  8282. }
  8283. }
  8284. return (changed > 0);
  8285. };
  8286. /**
  8287. * Create an items DOM
  8288. * @private
  8289. */
  8290. ItemRange.prototype._create = function _create() {
  8291. var dom = this.dom;
  8292. if (!dom) {
  8293. this.dom = dom = {};
  8294. // background box
  8295. dom.box = document.createElement('div');
  8296. // className is updated in repaint()
  8297. // contents box
  8298. dom.content = document.createElement('div');
  8299. dom.content.className = 'content';
  8300. dom.box.appendChild(dom.content);
  8301. }
  8302. };
  8303. /**
  8304. * Reposition the item, recalculate its left, top, and width, using the current
  8305. * range and size of the items itemset
  8306. * @override
  8307. */
  8308. ItemRange.prototype.reposition = function reposition() {
  8309. var dom = this.dom,
  8310. props = this.props;
  8311. if (dom) {
  8312. dom.box.style.top = this.top + 'px';
  8313. dom.box.style.left = this.left + 'px';
  8314. dom.box.style.width = this.width + 'px';
  8315. dom.content.style.left = props.content.left + 'px';
  8316. }
  8317. };
  8318. /**
  8319. * @constructor Group
  8320. * @param {GroupSet} parent
  8321. * @param {Number | String} groupId
  8322. * @param {Object} [options] Options to set initial property values
  8323. * // TODO: describe available options
  8324. * @extends Component
  8325. */
  8326. function Group (parent, groupId, options) {
  8327. this.id = util.randomUUID();
  8328. this.parent = parent;
  8329. this.groupId = groupId;
  8330. this.itemset = null; // ItemSet
  8331. this.options = options || {};
  8332. this.options.top = 0;
  8333. this.props = {
  8334. label: {
  8335. width: 0,
  8336. height: 0
  8337. }
  8338. };
  8339. this.top = 0;
  8340. this.left = 0;
  8341. this.width = 0;
  8342. this.height = 0;
  8343. }
  8344. Group.prototype = new Component();
  8345. // TODO: comment
  8346. Group.prototype.setOptions = Component.prototype.setOptions;
  8347. /**
  8348. * Get the container element of the panel, which can be used by a child to
  8349. * add its own widgets.
  8350. * @returns {HTMLElement} container
  8351. */
  8352. Group.prototype.getContainer = function () {
  8353. return this.parent.getContainer();
  8354. };
  8355. /**
  8356. * Set item set for the group. The group will create a view on the itemset,
  8357. * filtered by the groups id.
  8358. * @param {DataSet | DataView} items
  8359. */
  8360. Group.prototype.setItems = function setItems(items) {
  8361. if (this.itemset) {
  8362. // remove current item set
  8363. this.itemset.hide();
  8364. this.itemset.setItems();
  8365. this.parent.controller.remove(this.itemset);
  8366. this.itemset = null;
  8367. }
  8368. if (items) {
  8369. var groupId = this.groupId;
  8370. var itemsetOptions = Object.create(this.options);
  8371. this.itemset = new ItemSet(this, null, itemsetOptions);
  8372. this.itemset.setRange(this.parent.range);
  8373. this.view = new DataView(items, {
  8374. filter: function (item) {
  8375. return item.group == groupId;
  8376. }
  8377. });
  8378. this.itemset.setItems(this.view);
  8379. this.parent.controller.add(this.itemset);
  8380. }
  8381. };
  8382. /**
  8383. * Repaint the item
  8384. * @return {Boolean} changed
  8385. */
  8386. Group.prototype.repaint = function repaint() {
  8387. return false;
  8388. };
  8389. /**
  8390. * Reflow the item
  8391. * @return {Boolean} resized
  8392. */
  8393. Group.prototype.reflow = function reflow() {
  8394. var changed = 0,
  8395. update = util.updateProperty;
  8396. changed += update(this, 'top', this.itemset ? this.itemset.top : 0);
  8397. changed += update(this, 'height', this.itemset ? this.itemset.height : 0);
  8398. // TODO: reckon with the height of the group label
  8399. if (this.label) {
  8400. var inner = this.label.firstChild;
  8401. changed += update(this.props.label, 'width', inner.clientWidth);
  8402. changed += update(this.props.label, 'height', inner.clientHeight);
  8403. }
  8404. else {
  8405. changed += update(this.props.label, 'width', 0);
  8406. changed += update(this.props.label, 'height', 0);
  8407. }
  8408. return (changed > 0);
  8409. };
  8410. /**
  8411. * An GroupSet holds a set of groups
  8412. * @param {Component} parent
  8413. * @param {Component[]} [depends] Components on which this components depends
  8414. * (except for the parent)
  8415. * @param {Object} [options] See GroupSet.setOptions for the available
  8416. * options.
  8417. * @constructor GroupSet
  8418. * @extends Panel
  8419. */
  8420. function GroupSet(parent, depends, options) {
  8421. this.id = util.randomUUID();
  8422. this.parent = parent;
  8423. this.depends = depends;
  8424. this.options = options || {};
  8425. this.range = null; // Range or Object {start: number, end: number}
  8426. this.itemsData = null; // DataSet with items
  8427. this.groupsData = null; // DataSet with groups
  8428. this.groups = {}; // map with groups
  8429. this.dom = {};
  8430. this.props = {
  8431. labels: {
  8432. width: 0
  8433. }
  8434. };
  8435. // TODO: implement right orientation of the labels
  8436. // changes in groups are queued key/value map containing id/action
  8437. this.queue = {};
  8438. var me = this;
  8439. this.listeners = {
  8440. 'add': function (event, params) {
  8441. me._onAdd(params.items);
  8442. },
  8443. 'update': function (event, params) {
  8444. me._onUpdate(params.items);
  8445. },
  8446. 'remove': function (event, params) {
  8447. me._onRemove(params.items);
  8448. }
  8449. };
  8450. }
  8451. GroupSet.prototype = new Panel();
  8452. /**
  8453. * Set options for the GroupSet. Existing options will be extended/overwritten.
  8454. * @param {Object} [options] The following options are available:
  8455. * {String | function} groupsOrder
  8456. * TODO: describe options
  8457. */
  8458. GroupSet.prototype.setOptions = Component.prototype.setOptions;
  8459. GroupSet.prototype.setRange = function (range) {
  8460. // TODO: implement setRange
  8461. };
  8462. /**
  8463. * Set items
  8464. * @param {vis.DataSet | null} items
  8465. */
  8466. GroupSet.prototype.setItems = function setItems(items) {
  8467. this.itemsData = items;
  8468. for (var id in this.groups) {
  8469. if (this.groups.hasOwnProperty(id)) {
  8470. var group = this.groups[id];
  8471. group.setItems(items);
  8472. }
  8473. }
  8474. };
  8475. /**
  8476. * Get items
  8477. * @return {vis.DataSet | null} items
  8478. */
  8479. GroupSet.prototype.getItems = function getItems() {
  8480. return this.itemsData;
  8481. };
  8482. /**
  8483. * Set range (start and end).
  8484. * @param {Range | Object} range A Range or an object containing start and end.
  8485. */
  8486. GroupSet.prototype.setRange = function setRange(range) {
  8487. this.range = range;
  8488. };
  8489. /**
  8490. * Set groups
  8491. * @param {vis.DataSet} groups
  8492. */
  8493. GroupSet.prototype.setGroups = function setGroups(groups) {
  8494. var me = this,
  8495. ids;
  8496. // unsubscribe from current dataset
  8497. if (this.groupsData) {
  8498. util.forEach(this.listeners, function (callback, event) {
  8499. me.groupsData.unsubscribe(event, callback);
  8500. });
  8501. // remove all drawn groups
  8502. ids = this.groupsData.getIds();
  8503. this._onRemove(ids);
  8504. }
  8505. // replace the dataset
  8506. if (!groups) {
  8507. this.groupsData = null;
  8508. }
  8509. else if (groups instanceof DataSet) {
  8510. this.groupsData = groups;
  8511. }
  8512. else {
  8513. this.groupsData = new DataSet({
  8514. convert: {
  8515. start: 'Date',
  8516. end: 'Date'
  8517. }
  8518. });
  8519. this.groupsData.add(groups);
  8520. }
  8521. if (this.groupsData) {
  8522. // subscribe to new dataset
  8523. var id = this.id;
  8524. util.forEach(this.listeners, function (callback, event) {
  8525. me.groupsData.subscribe(event, callback, id);
  8526. });
  8527. // draw all new groups
  8528. ids = this.groupsData.getIds();
  8529. this._onAdd(ids);
  8530. }
  8531. };
  8532. /**
  8533. * Get groups
  8534. * @return {vis.DataSet | null} groups
  8535. */
  8536. GroupSet.prototype.getGroups = function getGroups() {
  8537. return this.groupsData;
  8538. };
  8539. /**
  8540. * Repaint the component
  8541. * @return {Boolean} changed
  8542. */
  8543. GroupSet.prototype.repaint = function repaint() {
  8544. var changed = 0,
  8545. i, id, group, label,
  8546. update = util.updateProperty,
  8547. asSize = util.option.asSize,
  8548. asElement = util.option.asElement,
  8549. options = this.options,
  8550. frame = this.dom.frame,
  8551. labels = this.dom.labels;
  8552. // create frame
  8553. if (!this.parent) {
  8554. throw new Error('Cannot repaint groupset: no parent attached');
  8555. }
  8556. var parentContainer = this.parent.getContainer();
  8557. if (!parentContainer) {
  8558. throw new Error('Cannot repaint groupset: parent has no container element');
  8559. }
  8560. if (!frame) {
  8561. frame = document.createElement('div');
  8562. frame.className = 'groupset';
  8563. this.dom.frame = frame;
  8564. var className = options.className;
  8565. if (className) {
  8566. util.addClassName(frame, util.option.asString(className));
  8567. }
  8568. changed += 1;
  8569. }
  8570. if (!frame.parentNode) {
  8571. parentContainer.appendChild(frame);
  8572. changed += 1;
  8573. }
  8574. // create labels
  8575. var labelContainer = asElement(options.labelContainer);
  8576. if (!labelContainer) {
  8577. throw new Error('Cannot repaint groupset: option "labelContainer" not defined');
  8578. }
  8579. if (!labels) {
  8580. labels = document.createElement('div');
  8581. labels.className = 'labels';
  8582. //frame.appendChild(labels);
  8583. this.dom.labels = labels;
  8584. }
  8585. if (!labels.parentNode || labels.parentNode != labelContainer) {
  8586. if (labels.parentNode) {
  8587. labels.parentNode.removeChild(labels.parentNode);
  8588. }
  8589. labelContainer.appendChild(labels);
  8590. }
  8591. // reposition frame
  8592. changed += update(frame.style, 'height', asSize(options.height, this.height + 'px'));
  8593. changed += update(frame.style, 'top', asSize(options.top, '0px'));
  8594. changed += update(frame.style, 'left', asSize(options.left, '0px'));
  8595. changed += update(frame.style, 'width', asSize(options.width, '100%'));
  8596. // reposition labels
  8597. changed += update(labels.style, 'top', asSize(options.top, '0px'));
  8598. var me = this,
  8599. queue = this.queue,
  8600. groups = this.groups,
  8601. groupsData = this.groupsData;
  8602. // show/hide added/changed/removed groups
  8603. var ids = Object.keys(queue);
  8604. if (ids.length) {
  8605. ids.forEach(function (id) {
  8606. var action = queue[id];
  8607. var group = groups[id];
  8608. //noinspection FallthroughInSwitchStatementJS
  8609. switch (action) {
  8610. case 'add':
  8611. case 'update':
  8612. if (!group) {
  8613. var groupOptions = Object.create(me.options);
  8614. group = new Group(me, id, groupOptions);
  8615. group.setItems(me.itemsData); // attach items data
  8616. groups[id] = group;
  8617. me.controller.add(group);
  8618. }
  8619. // TODO: update group data
  8620. group.data = groupsData.get(id);
  8621. delete queue[id];
  8622. break;
  8623. case 'remove':
  8624. if (group) {
  8625. group.setItems(); // detach items data
  8626. delete groups[id];
  8627. me.controller.remove(group);
  8628. }
  8629. // update lists
  8630. delete queue[id];
  8631. break;
  8632. default:
  8633. console.log('Error: unknown action "' + action + '"');
  8634. }
  8635. });
  8636. // the groupset depends on each of the groups
  8637. //this.depends = this.groups; // TODO: gives a circular reference through the parent
  8638. // TODO: apply dependencies of the groupset
  8639. // update the top positions of the groups in the correct order
  8640. var orderedGroups = this.groupsData.getIds({
  8641. order: this.options.groupsOrder
  8642. });
  8643. for (i = 0; i < orderedGroups.length; i++) {
  8644. (function (group, prevGroup) {
  8645. var top = 0;
  8646. if (prevGroup) {
  8647. top = function () {
  8648. // TODO: top must reckon with options.maxHeight
  8649. return prevGroup.top + prevGroup.height;
  8650. }
  8651. }
  8652. group.setOptions({
  8653. top: top
  8654. });
  8655. })(groups[orderedGroups[i]], groups[orderedGroups[i - 1]]);
  8656. }
  8657. // (re)create the labels
  8658. while (labels.firstChild) {
  8659. labels.removeChild(labels.firstChild);
  8660. }
  8661. for (i = 0; i < orderedGroups.length; i++) {
  8662. id = orderedGroups[i];
  8663. label = this._createLabel(id);
  8664. labels.appendChild(label);
  8665. }
  8666. changed++;
  8667. }
  8668. // reposition the labels
  8669. // TODO: labels are not displayed correctly when orientation=='top'
  8670. // TODO: width of labelPanel is not immediately updated on a change in groups
  8671. for (id in groups) {
  8672. if (groups.hasOwnProperty(id)) {
  8673. group = groups[id];
  8674. label = group.label;
  8675. if (label) {
  8676. label.style.top = group.top + 'px';
  8677. label.style.height = group.height + 'px';
  8678. }
  8679. }
  8680. }
  8681. return (changed > 0);
  8682. };
  8683. /**
  8684. * Create a label for group with given id
  8685. * @param {Number} id
  8686. * @return {Element} label
  8687. * @private
  8688. */
  8689. GroupSet.prototype._createLabel = function(id) {
  8690. var group = this.groups[id];
  8691. var label = document.createElement('div');
  8692. label.className = 'label';
  8693. var inner = document.createElement('div');
  8694. inner.className = 'inner';
  8695. label.appendChild(inner);
  8696. var content = group.data && group.data.content;
  8697. if (content instanceof Element) {
  8698. inner.appendChild(content);
  8699. }
  8700. else if (content != undefined) {
  8701. inner.innerHTML = content;
  8702. }
  8703. var className = group.data && group.data.className;
  8704. if (className) {
  8705. util.addClassName(label, className);
  8706. }
  8707. group.label = label; // TODO: not so nice, parking labels in the group this way!!!
  8708. return label;
  8709. };
  8710. /**
  8711. * Get container element
  8712. * @return {HTMLElement} container
  8713. */
  8714. GroupSet.prototype.getContainer = function getContainer() {
  8715. return this.dom.frame;
  8716. };
  8717. /**
  8718. * Get the width of the group labels
  8719. * @return {Number} width
  8720. */
  8721. GroupSet.prototype.getLabelsWidth = function getContainer() {
  8722. return this.props.labels.width;
  8723. };
  8724. /**
  8725. * Reflow the component
  8726. * @return {Boolean} resized
  8727. */
  8728. GroupSet.prototype.reflow = function reflow() {
  8729. var changed = 0,
  8730. id, group,
  8731. options = this.options,
  8732. update = util.updateProperty,
  8733. asNumber = util.option.asNumber,
  8734. asSize = util.option.asSize,
  8735. frame = this.dom.frame;
  8736. if (frame) {
  8737. var maxHeight = asNumber(options.maxHeight);
  8738. var fixedHeight = (asSize(options.height) != null);
  8739. var height;
  8740. if (fixedHeight) {
  8741. height = frame.offsetHeight;
  8742. }
  8743. else {
  8744. // height is not specified, calculate the sum of the height of all groups
  8745. height = 0;
  8746. for (id in this.groups) {
  8747. if (this.groups.hasOwnProperty(id)) {
  8748. group = this.groups[id];
  8749. height += group.height;
  8750. }
  8751. }
  8752. }
  8753. if (maxHeight != null) {
  8754. height = Math.min(height, maxHeight);
  8755. }
  8756. changed += update(this, 'height', height);
  8757. changed += update(this, 'top', frame.offsetTop);
  8758. changed += update(this, 'left', frame.offsetLeft);
  8759. changed += update(this, 'width', frame.offsetWidth);
  8760. }
  8761. // calculate the maximum width of the labels
  8762. var width = 0;
  8763. for (id in this.groups) {
  8764. if (this.groups.hasOwnProperty(id)) {
  8765. group = this.groups[id];
  8766. var labelWidth = group.props && group.props.label && group.props.label.width || 0;
  8767. width = Math.max(width, labelWidth);
  8768. }
  8769. }
  8770. changed += update(this.props.labels, 'width', width);
  8771. return (changed > 0);
  8772. };
  8773. /**
  8774. * Hide the component from the DOM
  8775. * @return {Boolean} changed
  8776. */
  8777. GroupSet.prototype.hide = function hide() {
  8778. if (this.dom.frame && this.dom.frame.parentNode) {
  8779. this.dom.frame.parentNode.removeChild(this.dom.frame);
  8780. return true;
  8781. }
  8782. else {
  8783. return false;
  8784. }
  8785. };
  8786. /**
  8787. * Show the component in the DOM (when not already visible).
  8788. * A repaint will be executed when the component is not visible
  8789. * @return {Boolean} changed
  8790. */
  8791. GroupSet.prototype.show = function show() {
  8792. if (!this.dom.frame || !this.dom.frame.parentNode) {
  8793. return this.repaint();
  8794. }
  8795. else {
  8796. return false;
  8797. }
  8798. };
  8799. /**
  8800. * Handle updated groups
  8801. * @param {Number[]} ids
  8802. * @private
  8803. */
  8804. GroupSet.prototype._onUpdate = function _onUpdate(ids) {
  8805. this._toQueue(ids, 'update');
  8806. };
  8807. /**
  8808. * Handle changed groups
  8809. * @param {Number[]} ids
  8810. * @private
  8811. */
  8812. GroupSet.prototype._onAdd = function _onAdd(ids) {
  8813. this._toQueue(ids, 'add');
  8814. };
  8815. /**
  8816. * Handle removed groups
  8817. * @param {Number[]} ids
  8818. * @private
  8819. */
  8820. GroupSet.prototype._onRemove = function _onRemove(ids) {
  8821. this._toQueue(ids, 'remove');
  8822. };
  8823. /**
  8824. * Put groups in the queue to be added/updated/remove
  8825. * @param {Number[]} ids
  8826. * @param {String} action can be 'add', 'update', 'remove'
  8827. */
  8828. GroupSet.prototype._toQueue = function _toQueue(ids, action) {
  8829. var queue = this.queue;
  8830. ids.forEach(function (id) {
  8831. queue[id] = action;
  8832. });
  8833. if (this.controller) {
  8834. //this.requestReflow();
  8835. this.requestRepaint();
  8836. }
  8837. };
  8838. /**
  8839. * Create a timeline visualization
  8840. * @param {HTMLElement} container
  8841. * @param {vis.DataSet | Array | DataTable} [items]
  8842. * @param {Object} [options] See Timeline.setOptions for the available options.
  8843. * @constructor
  8844. */
  8845. function Timeline (container, items, options) {
  8846. var me = this;
  8847. this.options = util.extend({
  8848. orientation: 'bottom',
  8849. min: null,
  8850. max: null,
  8851. zoomMin: 10, // milliseconds
  8852. zoomMax: 1000 * 60 * 60 * 24 * 365 * 10000, // milliseconds
  8853. // moveable: true, // TODO: option moveable
  8854. // zoomable: true, // TODO: option zoomable
  8855. showMinorLabels: true,
  8856. showMajorLabels: true,
  8857. autoResize: false
  8858. }, options);
  8859. // controller
  8860. this.controller = new Controller();
  8861. // root panel
  8862. if (!container) {
  8863. throw new Error('No container element provided');
  8864. }
  8865. var rootOptions = Object.create(this.options);
  8866. rootOptions.height = function () {
  8867. if (me.options.height) {
  8868. // fixed height
  8869. return me.options.height;
  8870. }
  8871. else {
  8872. // auto height
  8873. return me.timeaxis.height + me.content.height;
  8874. }
  8875. };
  8876. this.rootPanel = new RootPanel(container, rootOptions);
  8877. this.controller.add(this.rootPanel);
  8878. // item panel
  8879. var itemOptions = Object.create(this.options);
  8880. itemOptions.left = function () {
  8881. return me.labelPanel.width;
  8882. };
  8883. itemOptions.width = function () {
  8884. return me.rootPanel.width - me.labelPanel.width;
  8885. };
  8886. itemOptions.top = null;
  8887. itemOptions.height = null;
  8888. this.itemPanel = new Panel(this.rootPanel, [], itemOptions);
  8889. this.controller.add(this.itemPanel);
  8890. // label panel
  8891. var labelOptions = Object.create(this.options);
  8892. labelOptions.top = null;
  8893. labelOptions.left = null;
  8894. labelOptions.height = null;
  8895. labelOptions.width = function () {
  8896. if (me.content && typeof me.content.getLabelsWidth === 'function') {
  8897. return me.content.getLabelsWidth();
  8898. }
  8899. else {
  8900. return 0;
  8901. }
  8902. };
  8903. this.labelPanel = new Panel(this.rootPanel, [], labelOptions);
  8904. this.controller.add(this.labelPanel);
  8905. // range
  8906. var now = moment().hours(0).minutes(0).seconds(0).milliseconds(0);
  8907. this.range = new Range({
  8908. start: now.clone().add('days', -3).valueOf(),
  8909. end: now.clone().add('days', 4).valueOf()
  8910. });
  8911. /* TODO: fix range options
  8912. var rangeOptions = Object.create(this.options);
  8913. this.range = new Range(rangeOptions);
  8914. this.range.setRange(
  8915. now.clone().add('days', -3).valueOf(),
  8916. now.clone().add('days', 4).valueOf()
  8917. );
  8918. */
  8919. // TODO: reckon with options moveable and zoomable
  8920. this.range.subscribe(this.rootPanel, 'move', 'horizontal');
  8921. this.range.subscribe(this.rootPanel, 'zoom', 'horizontal');
  8922. this.range.on('rangechange', function () {
  8923. var force = true;
  8924. me.controller.requestReflow(force);
  8925. });
  8926. this.range.on('rangechanged', function () {
  8927. var force = true;
  8928. me.controller.requestReflow(force);
  8929. });
  8930. // TODO: put the listeners in setOptions, be able to dynamically change with options moveable and zoomable
  8931. // time axis
  8932. var timeaxisOptions = Object.create(rootOptions);
  8933. timeaxisOptions.range = this.range;
  8934. timeaxisOptions.left = null;
  8935. timeaxisOptions.top = null;
  8936. timeaxisOptions.width = '100%';
  8937. timeaxisOptions.height = null;
  8938. this.timeaxis = new TimeAxis(this.itemPanel, [], timeaxisOptions);
  8939. this.timeaxis.setRange(this.range);
  8940. this.controller.add(this.timeaxis);
  8941. // create itemset or groupset
  8942. this.setGroups(null);
  8943. this.itemsData = null; // DataSet
  8944. this.groupsData = null; // DataSet
  8945. // set data
  8946. if (items) {
  8947. this.setItems(items);
  8948. }
  8949. }
  8950. /**
  8951. * Set options
  8952. * @param {Object} options TODO: describe the available options
  8953. */
  8954. Timeline.prototype.setOptions = function (options) {
  8955. if (options) {
  8956. util.extend(this.options, options);
  8957. }
  8958. // TODO: apply range min,max
  8959. this.controller.reflow();
  8960. this.controller.repaint();
  8961. };
  8962. /**
  8963. * Set items
  8964. * @param {vis.DataSet | Array | DataTable | null} items
  8965. */
  8966. Timeline.prototype.setItems = function(items) {
  8967. var initialLoad = (this.itemsData == null);
  8968. // convert to type DataSet when needed
  8969. var newItemSet;
  8970. if (!items) {
  8971. newItemSet = null;
  8972. }
  8973. else if (items instanceof DataSet) {
  8974. newItemSet = items;
  8975. }
  8976. if (!(items instanceof DataSet)) {
  8977. newItemSet = new DataSet({
  8978. convert: {
  8979. start: 'Date',
  8980. end: 'Date'
  8981. }
  8982. });
  8983. newItemSet.add(items);
  8984. }
  8985. // set items
  8986. this.itemsData = newItemSet;
  8987. this.content.setItems(newItemSet);
  8988. if (initialLoad && (this.options.start == undefined || this.options.end == undefined)) {
  8989. // apply the data range as range
  8990. var dataRange = this.getItemRange();
  8991. // add 5% on both sides
  8992. var min = dataRange.min;
  8993. var max = dataRange.max;
  8994. if (min != null && max != null) {
  8995. var interval = (max.valueOf() - min.valueOf());
  8996. if (interval <= 0) {
  8997. // prevent an empty interval
  8998. interval = 24 * 60 * 60 * 1000; // 1 day
  8999. }
  9000. min = new Date(min.valueOf() - interval * 0.05);
  9001. max = new Date(max.valueOf() + interval * 0.05);
  9002. }
  9003. // override specified start and/or end date
  9004. if (this.options.start != undefined) {
  9005. min = new Date(this.options.start.valueOf());
  9006. }
  9007. if (this.options.end != undefined) {
  9008. max = new Date(this.options.end.valueOf());
  9009. }
  9010. // apply range if there is a min or max available
  9011. if (min != null || max != null) {
  9012. this.range.setRange(min, max);
  9013. }
  9014. }
  9015. };
  9016. /**
  9017. * Set groups
  9018. * @param {vis.DataSet | Array | DataTable} groups
  9019. */
  9020. Timeline.prototype.setGroups = function(groups) {
  9021. var me = this;
  9022. this.groupsData = groups;
  9023. // switch content type between ItemSet or GroupSet when needed
  9024. var type = this.groupsData ? GroupSet : ItemSet;
  9025. if (!(this.content instanceof type)) {
  9026. // remove old content set
  9027. if (this.content) {
  9028. this.content.hide();
  9029. if (this.content.setItems) {
  9030. this.content.setItems(); // disconnect from items
  9031. }
  9032. if (this.content.setGroups) {
  9033. this.content.setGroups(); // disconnect from groups
  9034. }
  9035. this.controller.remove(this.content);
  9036. }
  9037. // create new content set
  9038. var options = Object.create(this.options);
  9039. util.extend(options, {
  9040. top: function () {
  9041. if (me.options.orientation == 'top') {
  9042. return me.timeaxis.height;
  9043. }
  9044. else {
  9045. return me.itemPanel.height - me.timeaxis.height - me.content.height;
  9046. }
  9047. },
  9048. left: null,
  9049. width: '100%',
  9050. height: function () {
  9051. if (me.options.height) {
  9052. return me.itemPanel.height - me.timeaxis.height;
  9053. }
  9054. else {
  9055. return null;
  9056. }
  9057. },
  9058. maxHeight: function () {
  9059. if (me.options.maxHeight) {
  9060. if (!util.isNumber(me.options.maxHeight)) {
  9061. throw new TypeError('Number expected for property maxHeight');
  9062. }
  9063. return me.options.maxHeight - me.timeaxis.height;
  9064. }
  9065. else {
  9066. return null;
  9067. }
  9068. },
  9069. labelContainer: function () {
  9070. return me.labelPanel.getContainer();
  9071. }
  9072. });
  9073. this.content = new type(this.itemPanel, [this.timeaxis], options);
  9074. if (this.content.setRange) {
  9075. this.content.setRange(this.range);
  9076. }
  9077. if (this.content.setItems) {
  9078. this.content.setItems(this.itemsData);
  9079. }
  9080. if (this.content.setGroups) {
  9081. this.content.setGroups(this.groupsData);
  9082. }
  9083. this.controller.add(this.content);
  9084. }
  9085. };
  9086. /**
  9087. * Get the data range of the item set.
  9088. * @returns {{min: Date, max: Date}} range A range with a start and end Date.
  9089. * When no minimum is found, min==null
  9090. * When no maximum is found, max==null
  9091. */
  9092. Timeline.prototype.getItemRange = function getItemRange() {
  9093. // calculate min from start filed
  9094. var itemsData = this.itemsData,
  9095. min = null,
  9096. max = null;
  9097. if (itemsData) {
  9098. // calculate the minimum value of the field 'start'
  9099. var minItem = itemsData.min('start');
  9100. min = minItem ? minItem.start.valueOf() : null;
  9101. // calculate maximum value of fields 'start' and 'end'
  9102. var maxStartItem = itemsData.max('start');
  9103. if (maxStartItem) {
  9104. max = maxStartItem.start.valueOf();
  9105. }
  9106. var maxEndItem = itemsData.max('end');
  9107. if (maxEndItem) {
  9108. if (max == null) {
  9109. max = maxEndItem.end.valueOf();
  9110. }
  9111. else {
  9112. max = Math.max(max, maxEndItem.end.valueOf());
  9113. }
  9114. }
  9115. }
  9116. return {
  9117. min: (min != null) ? new Date(min) : null,
  9118. max: (max != null) ? new Date(max) : null
  9119. };
  9120. };
  9121. (function(exports) {
  9122. /**
  9123. * Parse a text source containing data in DOT language into a JSON object.
  9124. * The object contains two lists: one with nodes and one with edges.
  9125. *
  9126. * DOT language reference: http://www.graphviz.org/doc/info/lang.html
  9127. *
  9128. * @param {String} data Text containing a graph in DOT-notation
  9129. * @return {Object} graph An object containing two parameters:
  9130. * {Object[]} nodes
  9131. * {Object[]} edges
  9132. */
  9133. function parseDOT (data) {
  9134. dot = data;
  9135. return parseGraph();
  9136. }
  9137. // token types enumeration
  9138. var TOKENTYPE = {
  9139. NULL : 0,
  9140. DELIMITER : 1,
  9141. IDENTIFIER: 2,
  9142. UNKNOWN : 3
  9143. };
  9144. // map with all delimiters
  9145. var DELIMITERS = {
  9146. '{': true,
  9147. '}': true,
  9148. '[': true,
  9149. ']': true,
  9150. ';': true,
  9151. '=': true,
  9152. ',': true,
  9153. '->': true,
  9154. '--': true
  9155. };
  9156. var dot = ''; // current dot file
  9157. var index = 0; // current index in dot file
  9158. var c = ''; // current token character in expr
  9159. var token = ''; // current token
  9160. var tokenType = TOKENTYPE.NULL; // type of the token
  9161. /**
  9162. * Get the first character from the dot file.
  9163. * The character is stored into the char c. If the end of the dot file is
  9164. * reached, the function puts an empty string in c.
  9165. */
  9166. function first() {
  9167. index = 0;
  9168. c = dot.charAt(0);
  9169. }
  9170. /**
  9171. * Get the next character from the dot file.
  9172. * The character is stored into the char c. If the end of the dot file is
  9173. * reached, the function puts an empty string in c.
  9174. */
  9175. function next() {
  9176. index++;
  9177. c = dot.charAt(index);
  9178. }
  9179. /**
  9180. * Preview the next character from the dot file.
  9181. * @return {String} cNext
  9182. */
  9183. function nextPreview() {
  9184. return dot.charAt(index + 1);
  9185. }
  9186. /**
  9187. * Test whether given character is alphabetic or numeric
  9188. * @param {String} c
  9189. * @return {Boolean} isAlphaNumeric
  9190. */
  9191. var regexAlphaNumeric = /[a-zA-Z_0-9.:#]/;
  9192. function isAlphaNumeric(c) {
  9193. return regexAlphaNumeric.test(c);
  9194. }
  9195. /**
  9196. * Merge all properties of object b into object b
  9197. * @param {Object} a
  9198. * @param {Object} b
  9199. * @return {Object} a
  9200. */
  9201. function merge (a, b) {
  9202. if (!a) {
  9203. a = {};
  9204. }
  9205. if (b) {
  9206. for (var name in b) {
  9207. if (b.hasOwnProperty(name)) {
  9208. a[name] = b[name];
  9209. }
  9210. }
  9211. }
  9212. return a;
  9213. }
  9214. /**
  9215. * Set a value in an object, where the provided parameter name can be a
  9216. * path with nested parameters. For example:
  9217. *
  9218. * var obj = {a: 2};
  9219. * setValue(obj, 'b.c', 3); // obj = {a: 2, b: {c: 3}}
  9220. *
  9221. * @param {Object} obj
  9222. * @param {String} path A parameter name or dot-separated parameter path,
  9223. * like "color.highlight.border".
  9224. * @param {*} value
  9225. */
  9226. function setValue(obj, path, value) {
  9227. var keys = path.split('.');
  9228. var o = obj;
  9229. while (keys.length) {
  9230. var key = keys.shift();
  9231. if (keys.length) {
  9232. // this isn't the end point
  9233. if (!o[key]) {
  9234. o[key] = {};
  9235. }
  9236. o = o[key];
  9237. }
  9238. else {
  9239. // this is the end point
  9240. o[key] = value;
  9241. }
  9242. }
  9243. }
  9244. /**
  9245. * Add a node to a graph object. If there is already a node with
  9246. * the same id, their attributes will be merged.
  9247. * @param {Object} graph
  9248. * @param {Object} node
  9249. */
  9250. function addNode(graph, node) {
  9251. var i, len;
  9252. var current = null;
  9253. // find root graph (in case of subgraph)
  9254. var graphs = [graph]; // list with all graphs from current graph to root graph
  9255. var root = graph;
  9256. while (root.parent) {
  9257. graphs.push(root.parent);
  9258. root = root.parent;
  9259. }
  9260. // find existing node (at root level) by its id
  9261. if (root.nodes) {
  9262. for (i = 0, len = root.nodes.length; i < len; i++) {
  9263. if (node.id === root.nodes[i].id) {
  9264. current = root.nodes[i];
  9265. break;
  9266. }
  9267. }
  9268. }
  9269. if (!current) {
  9270. // this is a new node
  9271. current = {
  9272. id: node.id
  9273. };
  9274. if (graph.node) {
  9275. // clone default attributes
  9276. current.attr = merge(current.attr, graph.node);
  9277. }
  9278. }
  9279. // add node to this (sub)graph and all its parent graphs
  9280. for (i = graphs.length - 1; i >= 0; i--) {
  9281. var g = graphs[i];
  9282. if (!g.nodes) {
  9283. g.nodes = [];
  9284. }
  9285. if (g.nodes.indexOf(current) == -1) {
  9286. g.nodes.push(current);
  9287. }
  9288. }
  9289. // merge attributes
  9290. if (node.attr) {
  9291. current.attr = merge(current.attr, node.attr);
  9292. }
  9293. }
  9294. /**
  9295. * Add an edge to a graph object
  9296. * @param {Object} graph
  9297. * @param {Object} edge
  9298. */
  9299. function addEdge(graph, edge) {
  9300. if (!graph.edges) {
  9301. graph.edges = [];
  9302. }
  9303. graph.edges.push(edge);
  9304. if (graph.edge) {
  9305. var attr = merge({}, graph.edge); // clone default attributes
  9306. edge.attr = merge(attr, edge.attr); // merge attributes
  9307. }
  9308. }
  9309. /**
  9310. * Create an edge to a graph object
  9311. * @param {Object} graph
  9312. * @param {String | Number | Object} from
  9313. * @param {String | Number | Object} to
  9314. * @param {String} type
  9315. * @param {Object | null} attr
  9316. * @return {Object} edge
  9317. */
  9318. function createEdge(graph, from, to, type, attr) {
  9319. var edge = {
  9320. from: from,
  9321. to: to,
  9322. type: type
  9323. };
  9324. if (graph.edge) {
  9325. edge.attr = merge({}, graph.edge); // clone default attributes
  9326. }
  9327. edge.attr = merge(edge.attr || {}, attr); // merge attributes
  9328. return edge;
  9329. }
  9330. /**
  9331. * Get next token in the current dot file.
  9332. * The token and token type are available as token and tokenType
  9333. */
  9334. function getToken() {
  9335. tokenType = TOKENTYPE.NULL;
  9336. token = '';
  9337. // skip over whitespaces
  9338. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  9339. next();
  9340. }
  9341. do {
  9342. var isComment = false;
  9343. // skip comment
  9344. if (c == '#') {
  9345. // find the previous non-space character
  9346. var i = index - 1;
  9347. while (dot.charAt(i) == ' ' || dot.charAt(i) == '\t') {
  9348. i--;
  9349. }
  9350. if (dot.charAt(i) == '\n' || dot.charAt(i) == '') {
  9351. // the # is at the start of a line, this is indeed a line comment
  9352. while (c != '' && c != '\n') {
  9353. next();
  9354. }
  9355. isComment = true;
  9356. }
  9357. }
  9358. if (c == '/' && nextPreview() == '/') {
  9359. // skip line comment
  9360. while (c != '' && c != '\n') {
  9361. next();
  9362. }
  9363. isComment = true;
  9364. }
  9365. if (c == '/' && nextPreview() == '*') {
  9366. // skip block comment
  9367. while (c != '') {
  9368. if (c == '*' && nextPreview() == '/') {
  9369. // end of block comment found. skip these last two characters
  9370. next();
  9371. next();
  9372. break;
  9373. }
  9374. else {
  9375. next();
  9376. }
  9377. }
  9378. isComment = true;
  9379. }
  9380. // skip over whitespaces
  9381. while (c == ' ' || c == '\t' || c == '\n' || c == '\r') { // space, tab, enter
  9382. next();
  9383. }
  9384. }
  9385. while (isComment);
  9386. // check for end of dot file
  9387. if (c == '') {
  9388. // token is still empty
  9389. tokenType = TOKENTYPE.DELIMITER;
  9390. return;
  9391. }
  9392. // check for delimiters consisting of 2 characters
  9393. var c2 = c + nextPreview();
  9394. if (DELIMITERS[c2]) {
  9395. tokenType = TOKENTYPE.DELIMITER;
  9396. token = c2;
  9397. next();
  9398. next();
  9399. return;
  9400. }
  9401. // check for delimiters consisting of 1 character
  9402. if (DELIMITERS[c]) {
  9403. tokenType = TOKENTYPE.DELIMITER;
  9404. token = c;
  9405. next();
  9406. return;
  9407. }
  9408. // check for an identifier (number or string)
  9409. // TODO: more precise parsing of numbers/strings (and the port separator ':')
  9410. if (isAlphaNumeric(c) || c == '-') {
  9411. token += c;
  9412. next();
  9413. while (isAlphaNumeric(c)) {
  9414. token += c;
  9415. next();
  9416. }
  9417. if (token == 'false') {
  9418. token = false; // convert to boolean
  9419. }
  9420. else if (token == 'true') {
  9421. token = true; // convert to boolean
  9422. }
  9423. else if (!isNaN(Number(token))) {
  9424. token = Number(token); // convert to number
  9425. }
  9426. tokenType = TOKENTYPE.IDENTIFIER;
  9427. return;
  9428. }
  9429. // check for a string enclosed by double quotes
  9430. if (c == '"') {
  9431. next();
  9432. while (c != '' && (c != '"' || (c == '"' && nextPreview() == '"'))) {
  9433. token += c;
  9434. if (c == '"') { // skip the escape character
  9435. next();
  9436. }
  9437. next();
  9438. }
  9439. if (c != '"') {
  9440. throw newSyntaxError('End of string " expected');
  9441. }
  9442. next();
  9443. tokenType = TOKENTYPE.IDENTIFIER;
  9444. return;
  9445. }
  9446. // something unknown is found, wrong characters, a syntax error
  9447. tokenType = TOKENTYPE.UNKNOWN;
  9448. while (c != '') {
  9449. token += c;
  9450. next();
  9451. }
  9452. throw new SyntaxError('Syntax error in part "' + chop(token, 30) + '"');
  9453. }
  9454. /**
  9455. * Parse a graph.
  9456. * @returns {Object} graph
  9457. */
  9458. function parseGraph() {
  9459. var graph = {};
  9460. first();
  9461. getToken();
  9462. // optional strict keyword
  9463. if (token == 'strict') {
  9464. graph.strict = true;
  9465. getToken();
  9466. }
  9467. // graph or digraph keyword
  9468. if (token == 'graph' || token == 'digraph') {
  9469. graph.type = token;
  9470. getToken();
  9471. }
  9472. // optional graph id
  9473. if (tokenType == TOKENTYPE.IDENTIFIER) {
  9474. graph.id = token;
  9475. getToken();
  9476. }
  9477. // open angle bracket
  9478. if (token != '{') {
  9479. throw newSyntaxError('Angle bracket { expected');
  9480. }
  9481. getToken();
  9482. // statements
  9483. parseStatements(graph);
  9484. // close angle bracket
  9485. if (token != '}') {
  9486. throw newSyntaxError('Angle bracket } expected');
  9487. }
  9488. getToken();
  9489. // end of file
  9490. if (token !== '') {
  9491. throw newSyntaxError('End of file expected');
  9492. }
  9493. getToken();
  9494. // remove temporary default properties
  9495. delete graph.node;
  9496. delete graph.edge;
  9497. delete graph.graph;
  9498. return graph;
  9499. }
  9500. /**
  9501. * Parse a list with statements.
  9502. * @param {Object} graph
  9503. */
  9504. function parseStatements (graph) {
  9505. while (token !== '' && token != '}') {
  9506. parseStatement(graph);
  9507. if (token == ';') {
  9508. getToken();
  9509. }
  9510. }
  9511. }
  9512. /**
  9513. * Parse a single statement. Can be a an attribute statement, node
  9514. * statement, a series of node statements and edge statements, or a
  9515. * parameter.
  9516. * @param {Object} graph
  9517. */
  9518. function parseStatement(graph) {
  9519. // parse subgraph
  9520. var subgraph = parseSubgraph(graph);
  9521. if (subgraph) {
  9522. // edge statements
  9523. parseEdge(graph, subgraph);
  9524. return;
  9525. }
  9526. // parse an attribute statement
  9527. var attr = parseAttributeStatement(graph);
  9528. if (attr) {
  9529. return;
  9530. }
  9531. // parse node
  9532. if (tokenType != TOKENTYPE.IDENTIFIER) {
  9533. throw newSyntaxError('Identifier expected');
  9534. }
  9535. var id = token; // id can be a string or a number
  9536. getToken();
  9537. if (token == '=') {
  9538. // id statement
  9539. getToken();
  9540. if (tokenType != TOKENTYPE.IDENTIFIER) {
  9541. throw newSyntaxError('Identifier expected');
  9542. }
  9543. graph[id] = token;
  9544. getToken();
  9545. // TODO: implement comma separated list with "a_list: ID=ID [','] [a_list] "
  9546. }
  9547. else {
  9548. parseNodeStatement(graph, id);
  9549. }
  9550. }
  9551. /**
  9552. * Parse a subgraph
  9553. * @param {Object} graph parent graph object
  9554. * @return {Object | null} subgraph
  9555. */
  9556. function parseSubgraph (graph) {
  9557. var subgraph = null;
  9558. // optional subgraph keyword
  9559. if (token == 'subgraph') {
  9560. subgraph = {};
  9561. subgraph.type = 'subgraph';
  9562. getToken();
  9563. // optional graph id
  9564. if (tokenType == TOKENTYPE.IDENTIFIER) {
  9565. subgraph.id = token;
  9566. getToken();
  9567. }
  9568. }
  9569. // open angle bracket
  9570. if (token == '{') {
  9571. getToken();
  9572. if (!subgraph) {
  9573. subgraph = {};
  9574. }
  9575. subgraph.parent = graph;
  9576. subgraph.node = graph.node;
  9577. subgraph.edge = graph.edge;
  9578. subgraph.graph = graph.graph;
  9579. // statements
  9580. parseStatements(subgraph);
  9581. // close angle bracket
  9582. if (token != '}') {
  9583. throw newSyntaxError('Angle bracket } expected');
  9584. }
  9585. getToken();
  9586. // remove temporary default properties
  9587. delete subgraph.node;
  9588. delete subgraph.edge;
  9589. delete subgraph.graph;
  9590. delete subgraph.parent;
  9591. // register at the parent graph
  9592. if (!graph.subgraphs) {
  9593. graph.subgraphs = [];
  9594. }
  9595. graph.subgraphs.push(subgraph);
  9596. }
  9597. return subgraph;
  9598. }
  9599. /**
  9600. * parse an attribute statement like "node [shape=circle fontSize=16]".
  9601. * Available keywords are 'node', 'edge', 'graph'.
  9602. * The previous list with default attributes will be replaced
  9603. * @param {Object} graph
  9604. * @returns {String | null} keyword Returns the name of the parsed attribute
  9605. * (node, edge, graph), or null if nothing
  9606. * is parsed.
  9607. */
  9608. function parseAttributeStatement (graph) {
  9609. // attribute statements
  9610. if (token == 'node') {
  9611. getToken();
  9612. // node attributes
  9613. graph.node = parseAttributeList();
  9614. return 'node';
  9615. }
  9616. else if (token == 'edge') {
  9617. getToken();
  9618. // edge attributes
  9619. graph.edge = parseAttributeList();
  9620. return 'edge';
  9621. }
  9622. else if (token == 'graph') {
  9623. getToken();
  9624. // graph attributes
  9625. graph.graph = parseAttributeList();
  9626. return 'graph';
  9627. }
  9628. return null;
  9629. }
  9630. /**
  9631. * parse a node statement
  9632. * @param {Object} graph
  9633. * @param {String | Number} id
  9634. */
  9635. function parseNodeStatement(graph, id) {
  9636. // node statement
  9637. var node = {
  9638. id: id
  9639. };
  9640. var attr = parseAttributeList();
  9641. if (attr) {
  9642. node.attr = attr;
  9643. }
  9644. addNode(graph, node);
  9645. // edge statements
  9646. parseEdge(graph, id);
  9647. }
  9648. /**
  9649. * Parse an edge or a series of edges
  9650. * @param {Object} graph
  9651. * @param {String | Number} from Id of the from node
  9652. */
  9653. function parseEdge(graph, from) {
  9654. while (token == '->' || token == '--') {
  9655. var to;
  9656. var type = token;
  9657. getToken();
  9658. var subgraph = parseSubgraph(graph);
  9659. if (subgraph) {
  9660. to = subgraph;
  9661. }
  9662. else {
  9663. if (tokenType != TOKENTYPE.IDENTIFIER) {
  9664. throw newSyntaxError('Identifier or subgraph expected');
  9665. }
  9666. to = token;
  9667. addNode(graph, {
  9668. id: to
  9669. });
  9670. getToken();
  9671. }
  9672. // parse edge attributes
  9673. var attr = parseAttributeList();
  9674. // create edge
  9675. var edge = createEdge(graph, from, to, type, attr);
  9676. addEdge(graph, edge);
  9677. from = to;
  9678. }
  9679. }
  9680. /**
  9681. * Parse a set with attributes,
  9682. * for example [label="1.000", shape=solid]
  9683. * @return {Object | null} attr
  9684. */
  9685. function parseAttributeList() {
  9686. var attr = null;
  9687. while (token == '[') {
  9688. getToken();
  9689. attr = {};
  9690. while (token !== '' && token != ']') {
  9691. if (tokenType != TOKENTYPE.IDENTIFIER) {
  9692. throw newSyntaxError('Attribute name expected');
  9693. }
  9694. var name = token;
  9695. getToken();
  9696. if (token != '=') {
  9697. throw newSyntaxError('Equal sign = expected');
  9698. }
  9699. getToken();
  9700. if (tokenType != TOKENTYPE.IDENTIFIER) {
  9701. throw newSyntaxError('Attribute value expected');
  9702. }
  9703. var value = token;
  9704. setValue(attr, name, value); // name can be a path
  9705. getToken();
  9706. if (token ==',') {
  9707. getToken();
  9708. }
  9709. }
  9710. if (token != ']') {
  9711. throw newSyntaxError('Bracket ] expected');
  9712. }
  9713. getToken();
  9714. }
  9715. return attr;
  9716. }
  9717. /**
  9718. * Create a syntax error with extra information on current token and index.
  9719. * @param {String} message
  9720. * @returns {SyntaxError} err
  9721. */
  9722. function newSyntaxError(message) {
  9723. return new SyntaxError(message + ', got "' + chop(token, 30) + '" (char ' + index + ')');
  9724. }
  9725. /**
  9726. * Chop off text after a maximum length
  9727. * @param {String} text
  9728. * @param {Number} maxLength
  9729. * @returns {String}
  9730. */
  9731. function chop (text, maxLength) {
  9732. return (text.length <= maxLength) ? text : (text.substr(0, 27) + '...');
  9733. }
  9734. /**
  9735. * Execute a function fn for each pair of elements in two arrays
  9736. * @param {Array | *} array1
  9737. * @param {Array | *} array2
  9738. * @param {function} fn
  9739. */
  9740. function forEach2(array1, array2, fn) {
  9741. if (array1 instanceof Array) {
  9742. array1.forEach(function (elem1) {
  9743. if (array2 instanceof Array) {
  9744. array2.forEach(function (elem2) {
  9745. fn(elem1, elem2);
  9746. });
  9747. }
  9748. else {
  9749. fn(elem1, array2);
  9750. }
  9751. });
  9752. }
  9753. else {
  9754. if (array2 instanceof Array) {
  9755. array2.forEach(function (elem2) {
  9756. fn(array1, elem2);
  9757. });
  9758. }
  9759. else {
  9760. fn(array1, array2);
  9761. }
  9762. }
  9763. }
  9764. /**
  9765. * Convert a string containing a graph in DOT language into a map containing
  9766. * with nodes and edges in the format of graph.
  9767. * @param {String} data Text containing a graph in DOT-notation
  9768. * @return {Object} graphData
  9769. */
  9770. function DOTToGraph (data) {
  9771. // parse the DOT file
  9772. var dotData = parseDOT(data);
  9773. var graphData = {
  9774. nodes: [],
  9775. edges: [],
  9776. options: {}
  9777. };
  9778. // copy the nodes
  9779. if (dotData.nodes) {
  9780. dotData.nodes.forEach(function (dotNode) {
  9781. var graphNode = {
  9782. id: dotNode.id,
  9783. label: String(dotNode.label || dotNode.id)
  9784. };
  9785. merge(graphNode, dotNode.attr);
  9786. if (graphNode.image) {
  9787. graphNode.shape = 'image';
  9788. }
  9789. graphData.nodes.push(graphNode);
  9790. });
  9791. }
  9792. // copy the edges
  9793. if (dotData.edges) {
  9794. /**
  9795. * Convert an edge in DOT format to an edge with VisGraph format
  9796. * @param {Object} dotEdge
  9797. * @returns {Object} graphEdge
  9798. */
  9799. function convertEdge(dotEdge) {
  9800. var graphEdge = {
  9801. from: dotEdge.from,
  9802. to: dotEdge.to
  9803. };
  9804. merge(graphEdge, dotEdge.attr);
  9805. graphEdge.style = (dotEdge.type == '->') ? 'arrow' : 'line';
  9806. return graphEdge;
  9807. }
  9808. dotData.edges.forEach(function (dotEdge) {
  9809. var from, to;
  9810. if (dotEdge.from instanceof Object) {
  9811. from = dotEdge.from.nodes;
  9812. }
  9813. else {
  9814. from = {
  9815. id: dotEdge.from
  9816. }
  9817. }
  9818. if (dotEdge.to instanceof Object) {
  9819. to = dotEdge.to.nodes;
  9820. }
  9821. else {
  9822. to = {
  9823. id: dotEdge.to
  9824. }
  9825. }
  9826. if (dotEdge.from instanceof Object && dotEdge.from.edges) {
  9827. dotEdge.from.edges.forEach(function (subEdge) {
  9828. var graphEdge = convertEdge(subEdge);
  9829. graphData.edges.push(graphEdge);
  9830. });
  9831. }
  9832. forEach2(from, to, function (from, to) {
  9833. var subEdge = createEdge(graphData, from.id, to.id, dotEdge.type, dotEdge.attr);
  9834. var graphEdge = convertEdge(subEdge);
  9835. graphData.edges.push(graphEdge);
  9836. });
  9837. if (dotEdge.to instanceof Object && dotEdge.to.edges) {
  9838. dotEdge.to.edges.forEach(function (subEdge) {
  9839. var graphEdge = convertEdge(subEdge);
  9840. graphData.edges.push(graphEdge);
  9841. });
  9842. }
  9843. });
  9844. }
  9845. // copy the options
  9846. if (dotData.attr) {
  9847. graphData.options = dotData.attr;
  9848. }
  9849. return graphData;
  9850. }
  9851. // exports
  9852. exports.parseDOT = parseDOT;
  9853. exports.DOTToGraph = DOTToGraph;
  9854. })(typeof util !== 'undefined' ? util : exports);
  9855. /**
  9856. * Canvas shapes used by the Graph
  9857. */
  9858. if (typeof CanvasRenderingContext2D !== 'undefined') {
  9859. /**
  9860. * Draw a circle shape
  9861. */
  9862. CanvasRenderingContext2D.prototype.circle = function(x, y, r) {
  9863. this.beginPath();
  9864. this.arc(x, y, r, 0, 2*Math.PI, false);
  9865. };
  9866. /**
  9867. * Draw a square shape
  9868. * @param {Number} x horizontal center
  9869. * @param {Number} y vertical center
  9870. * @param {Number} r size, width and height of the square
  9871. */
  9872. CanvasRenderingContext2D.prototype.square = function(x, y, r) {
  9873. this.beginPath();
  9874. this.rect(x - r, y - r, r * 2, r * 2);
  9875. };
  9876. /**
  9877. * Draw a triangle shape
  9878. * @param {Number} x horizontal center
  9879. * @param {Number} y vertical center
  9880. * @param {Number} r radius, half the length of the sides of the triangle
  9881. */
  9882. CanvasRenderingContext2D.prototype.triangle = function(x, y, r) {
  9883. // http://en.wikipedia.org/wiki/Equilateral_triangle
  9884. this.beginPath();
  9885. var s = r * 2;
  9886. var s2 = s / 2;
  9887. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  9888. var h = Math.sqrt(s * s - s2 * s2); // height
  9889. this.moveTo(x, y - (h - ir));
  9890. this.lineTo(x + s2, y + ir);
  9891. this.lineTo(x - s2, y + ir);
  9892. this.lineTo(x, y - (h - ir));
  9893. this.closePath();
  9894. };
  9895. /**
  9896. * Draw a triangle shape in downward orientation
  9897. * @param {Number} x horizontal center
  9898. * @param {Number} y vertical center
  9899. * @param {Number} r radius
  9900. */
  9901. CanvasRenderingContext2D.prototype.triangleDown = function(x, y, r) {
  9902. // http://en.wikipedia.org/wiki/Equilateral_triangle
  9903. this.beginPath();
  9904. var s = r * 2;
  9905. var s2 = s / 2;
  9906. var ir = Math.sqrt(3) / 6 * s; // radius of inner circle
  9907. var h = Math.sqrt(s * s - s2 * s2); // height
  9908. this.moveTo(x, y + (h - ir));
  9909. this.lineTo(x + s2, y - ir);
  9910. this.lineTo(x - s2, y - ir);
  9911. this.lineTo(x, y + (h - ir));
  9912. this.closePath();
  9913. };
  9914. /**
  9915. * Draw a star shape, a star with 5 points
  9916. * @param {Number} x horizontal center
  9917. * @param {Number} y vertical center
  9918. * @param {Number} r radius, half the length of the sides of the triangle
  9919. */
  9920. CanvasRenderingContext2D.prototype.star = function(x, y, r) {
  9921. // http://www.html5canvastutorials.com/labs/html5-canvas-star-spinner/
  9922. this.beginPath();
  9923. for (var n = 0; n < 10; n++) {
  9924. var radius = (n % 2 === 0) ? r * 1.3 : r * 0.5;
  9925. this.lineTo(
  9926. x + radius * Math.sin(n * 2 * Math.PI / 10),
  9927. y - radius * Math.cos(n * 2 * Math.PI / 10)
  9928. );
  9929. }
  9930. this.closePath();
  9931. };
  9932. /**
  9933. * http://stackoverflow.com/questions/1255512/how-to-draw-a-rounded-rectangle-on-html-canvas
  9934. */
  9935. CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
  9936. var r2d = Math.PI/180;
  9937. if( w - ( 2 * r ) < 0 ) { r = ( w / 2 ); } //ensure that the radius isn't too large for x
  9938. if( h - ( 2 * r ) < 0 ) { r = ( h / 2 ); } //ensure that the radius isn't too large for y
  9939. this.beginPath();
  9940. this.moveTo(x+r,y);
  9941. this.lineTo(x+w-r,y);
  9942. this.arc(x+w-r,y+r,r,r2d*270,r2d*360,false);
  9943. this.lineTo(x+w,y+h-r);
  9944. this.arc(x+w-r,y+h-r,r,0,r2d*90,false);
  9945. this.lineTo(x+r,y+h);
  9946. this.arc(x+r,y+h-r,r,r2d*90,r2d*180,false);
  9947. this.lineTo(x,y+r);
  9948. this.arc(x+r,y+r,r,r2d*180,r2d*270,false);
  9949. };
  9950. /**
  9951. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  9952. */
  9953. CanvasRenderingContext2D.prototype.ellipse = function(x, y, w, h) {
  9954. var kappa = .5522848,
  9955. ox = (w / 2) * kappa, // control point offset horizontal
  9956. oy = (h / 2) * kappa, // control point offset vertical
  9957. xe = x + w, // x-end
  9958. ye = y + h, // y-end
  9959. xm = x + w / 2, // x-middle
  9960. ym = y + h / 2; // y-middle
  9961. this.beginPath();
  9962. this.moveTo(x, ym);
  9963. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  9964. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  9965. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  9966. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  9967. };
  9968. /**
  9969. * http://stackoverflow.com/questions/2172798/how-to-draw-an-oval-in-html5-canvas
  9970. */
  9971. CanvasRenderingContext2D.prototype.database = function(x, y, w, h) {
  9972. var f = 1/3;
  9973. var wEllipse = w;
  9974. var hEllipse = h * f;
  9975. var kappa = .5522848,
  9976. ox = (wEllipse / 2) * kappa, // control point offset horizontal
  9977. oy = (hEllipse / 2) * kappa, // control point offset vertical
  9978. xe = x + wEllipse, // x-end
  9979. ye = y + hEllipse, // y-end
  9980. xm = x + wEllipse / 2, // x-middle
  9981. ym = y + hEllipse / 2, // y-middle
  9982. ymb = y + (h - hEllipse/2), // y-midlle, bottom ellipse
  9983. yeb = y + h; // y-end, bottom ellipse
  9984. this.beginPath();
  9985. this.moveTo(xe, ym);
  9986. this.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  9987. this.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  9988. this.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  9989. this.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  9990. this.lineTo(xe, ymb);
  9991. this.bezierCurveTo(xe, ymb + oy, xm + ox, yeb, xm, yeb);
  9992. this.bezierCurveTo(xm - ox, yeb, x, ymb + oy, x, ymb);
  9993. this.lineTo(x, ym);
  9994. };
  9995. /**
  9996. * Draw an arrow point (no line)
  9997. */
  9998. CanvasRenderingContext2D.prototype.arrow = function(x, y, angle, length) {
  9999. // tail
  10000. var xt = x - length * Math.cos(angle);
  10001. var yt = y - length * Math.sin(angle);
  10002. // inner tail
  10003. // TODO: allow to customize different shapes
  10004. var xi = x - length * 0.9 * Math.cos(angle);
  10005. var yi = y - length * 0.9 * Math.sin(angle);
  10006. // left
  10007. var xl = xt + length / 3 * Math.cos(angle + 0.5 * Math.PI);
  10008. var yl = yt + length / 3 * Math.sin(angle + 0.5 * Math.PI);
  10009. // right
  10010. var xr = xt + length / 3 * Math.cos(angle - 0.5 * Math.PI);
  10011. var yr = yt + length / 3 * Math.sin(angle - 0.5 * Math.PI);
  10012. this.beginPath();
  10013. this.moveTo(x, y);
  10014. this.lineTo(xl, yl);
  10015. this.lineTo(xi, yi);
  10016. this.lineTo(xr, yr);
  10017. this.closePath();
  10018. };
  10019. /**
  10020. * Sets up the dashedLine functionality for drawing
  10021. * Original code came from http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas
  10022. * @author David Jordan
  10023. * @date 2012-08-08
  10024. */
  10025. CanvasRenderingContext2D.prototype.dashedLine = function(x,y,x2,y2,dashArray){
  10026. if (!dashArray) dashArray=[10,5];
  10027. if (dashLength==0) dashLength = 0.001; // Hack for Safari
  10028. var dashCount = dashArray.length;
  10029. this.moveTo(x, y);
  10030. var dx = (x2-x), dy = (y2-y);
  10031. var slope = dy/dx;
  10032. var distRemaining = Math.sqrt( dx*dx + dy*dy );
  10033. var dashIndex=0, draw=true;
  10034. while (distRemaining>=0.1){
  10035. var dashLength = dashArray[dashIndex++%dashCount];
  10036. if (dashLength > distRemaining) dashLength = distRemaining;
  10037. var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
  10038. if (dx<0) xStep = -xStep;
  10039. x += xStep;
  10040. y += slope*xStep;
  10041. this[draw ? 'lineTo' : 'moveTo'](x,y);
  10042. distRemaining -= dashLength;
  10043. draw = !draw;
  10044. }
  10045. };
  10046. // TODO: add diamond shape
  10047. }
  10048. /**
  10049. * @class Node
  10050. * A node. A node can be connected to other nodes via one or multiple edges.
  10051. * @param {object} properties An object containing properties for the node. All
  10052. * properties are optional, except for the id.
  10053. * {number} id Id of the node. Required
  10054. * {string} label Text label for the node
  10055. * {number} x Horizontal position of the node
  10056. * {number} y Vertical position of the node
  10057. * {string} shape Node shape, available:
  10058. * "database", "circle", "ellipse",
  10059. * "box", "image", "text", "dot",
  10060. * "star", "triangle", "triangleDown",
  10061. * "square"
  10062. * {string} image An image url
  10063. * {string} title An title text, can be HTML
  10064. * {anytype} group A group name or number
  10065. * @param {Graph.Images} imagelist A list with images. Only needed
  10066. * when the node has an image
  10067. * @param {Graph.Groups} grouplist A list with groups. Needed for
  10068. * retrieving group properties
  10069. * @param {Object} constants An object with default values for
  10070. * example for the color
  10071. */
  10072. function Node(properties, imagelist, grouplist, constants) {
  10073. this.selected = false;
  10074. this.edges = []; // all edges connected to this node
  10075. this.group = constants.nodes.group;
  10076. this.fontSize = constants.nodes.fontSize;
  10077. this.fontFace = constants.nodes.fontFace;
  10078. this.fontColor = constants.nodes.fontColor;
  10079. this.color = constants.nodes.color;
  10080. // set defaults for the properties
  10081. this.id = undefined;
  10082. this.shape = constants.nodes.shape;
  10083. this.image = constants.nodes.image;
  10084. this.x = 0;
  10085. this.y = 0;
  10086. this.xFixed = false;
  10087. this.yFixed = false;
  10088. this.radius = constants.nodes.radius;
  10089. this.radiusFixed = false;
  10090. this.radiusMin = constants.nodes.radiusMin;
  10091. this.radiusMax = constants.nodes.radiusMax;
  10092. this.imagelist = imagelist;
  10093. this.grouplist = grouplist;
  10094. this.setProperties(properties, constants);
  10095. // mass, force, velocity
  10096. this.mass = 50; // kg (mass is adjusted for the number of connected edges)
  10097. this.fx = 0.0; // external force x
  10098. this.fy = 0.0; // external force y
  10099. this.vx = 0.0; // velocity x
  10100. this.vy = 0.0; // velocity y
  10101. this.minForce = constants.minForce;
  10102. this.damping = 0.9; // damping factor
  10103. };
  10104. /**
  10105. * Attach a edge to the node
  10106. * @param {Edge} edge
  10107. */
  10108. Node.prototype.attachEdge = function(edge) {
  10109. if (this.edges.indexOf(edge) == -1) {
  10110. this.edges.push(edge);
  10111. }
  10112. this._updateMass();
  10113. };
  10114. /**
  10115. * Detach a edge from the node
  10116. * @param {Edge} edge
  10117. */
  10118. Node.prototype.detachEdge = function(edge) {
  10119. var index = this.edges.indexOf(edge);
  10120. if (index != -1) {
  10121. this.edges.splice(index, 1);
  10122. }
  10123. this._updateMass();
  10124. };
  10125. /**
  10126. * Update the nodes mass, which is determined by the number of edges connecting
  10127. * to it (more edges -> heavier node).
  10128. * @private
  10129. */
  10130. Node.prototype._updateMass = function() {
  10131. this.mass = 50 + 20 * this.edges.length; // kg
  10132. };
  10133. /**
  10134. * Set or overwrite properties for the node
  10135. * @param {Object} properties an object with properties
  10136. * @param {Object} constants and object with default, global properties
  10137. */
  10138. Node.prototype.setProperties = function(properties, constants) {
  10139. if (!properties) {
  10140. return;
  10141. }
  10142. // basic properties
  10143. if (properties.id != undefined) {this.id = properties.id;}
  10144. if (properties.label != undefined) {this.label = properties.label;}
  10145. if (properties.title != undefined) {this.title = properties.title;}
  10146. if (properties.group != undefined) {this.group = properties.group;}
  10147. if (properties.x != undefined) {this.x = properties.x;}
  10148. if (properties.y != undefined) {this.y = properties.y;}
  10149. if (properties.value != undefined) {this.value = properties.value;}
  10150. if (this.id === undefined) {
  10151. throw "Node must have an id";
  10152. }
  10153. // copy group properties
  10154. if (this.group) {
  10155. var groupObj = this.grouplist.get(this.group);
  10156. for (var prop in groupObj) {
  10157. if (groupObj.hasOwnProperty(prop)) {
  10158. this[prop] = groupObj[prop];
  10159. }
  10160. }
  10161. }
  10162. // individual shape properties
  10163. if (properties.shape != undefined) {this.shape = properties.shape;}
  10164. if (properties.image != undefined) {this.image = properties.image;}
  10165. if (properties.radius != undefined) {this.radius = properties.radius;}
  10166. if (properties.color != undefined) {this.color = Node.parseColor(properties.color);}
  10167. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  10168. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  10169. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  10170. if (this.image != undefined) {
  10171. if (this.imagelist) {
  10172. this.imageObj = this.imagelist.load(this.image);
  10173. }
  10174. else {
  10175. throw "No imagelist provided";
  10176. }
  10177. }
  10178. this.xFixed = this.xFixed || (properties.x != undefined);
  10179. this.yFixed = this.yFixed || (properties.y != undefined);
  10180. this.radiusFixed = this.radiusFixed || (properties.radius != undefined);
  10181. if (this.shape == 'image') {
  10182. this.radiusMin = constants.nodes.widthMin;
  10183. this.radiusMax = constants.nodes.widthMax;
  10184. }
  10185. // choose draw method depending on the shape
  10186. switch (this.shape) {
  10187. case 'database': this.draw = this._drawDatabase; this.resize = this._resizeDatabase; break;
  10188. case 'box': this.draw = this._drawBox; this.resize = this._resizeBox; break;
  10189. case 'circle': this.draw = this._drawCircle; this.resize = this._resizeCircle; break;
  10190. case 'ellipse': this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10191. // TODO: add diamond shape
  10192. case 'image': this.draw = this._drawImage; this.resize = this._resizeImage; break;
  10193. case 'text': this.draw = this._drawText; this.resize = this._resizeText; break;
  10194. case 'dot': this.draw = this._drawDot; this.resize = this._resizeShape; break;
  10195. case 'square': this.draw = this._drawSquare; this.resize = this._resizeShape; break;
  10196. case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
  10197. case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
  10198. case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
  10199. default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
  10200. }
  10201. // reset the size of the node, this can be changed
  10202. this._reset();
  10203. };
  10204. /**
  10205. * Parse a color property into an object with border, background, and
  10206. * hightlight colors
  10207. * @param {Object | String} color
  10208. * @return {Object} colorObject
  10209. */
  10210. Node.parseColor = function(color) {
  10211. var c;
  10212. if (util.isString(color)) {
  10213. c = {
  10214. border: color,
  10215. background: color,
  10216. highlight: {
  10217. border: color,
  10218. background: color
  10219. }
  10220. };
  10221. // TODO: automatically generate a nice highlight color
  10222. }
  10223. else {
  10224. c = {};
  10225. c.background = color.background || 'white';
  10226. c.border = color.border || c.background;
  10227. if (util.isString(color.highlight)) {
  10228. c.highlight = {
  10229. border: color.highlight,
  10230. background: color.highlight
  10231. }
  10232. }
  10233. else {
  10234. c.highlight = {};
  10235. c.highlight.background = color.highlight && color.highlight.background || c.background;
  10236. c.highlight.border = color.highlight && color.highlight.border || c.border;
  10237. }
  10238. }
  10239. return c;
  10240. };
  10241. /**
  10242. * select this node
  10243. */
  10244. Node.prototype.select = function() {
  10245. this.selected = true;
  10246. this._reset();
  10247. };
  10248. /**
  10249. * unselect this node
  10250. */
  10251. Node.prototype.unselect = function() {
  10252. this.selected = false;
  10253. this._reset();
  10254. };
  10255. /**
  10256. * Reset the calculated size of the node, forces it to recalculate its size
  10257. * @private
  10258. */
  10259. Node.prototype._reset = function() {
  10260. this.width = undefined;
  10261. this.height = undefined;
  10262. };
  10263. /**
  10264. * get the title of this node.
  10265. * @return {string} title The title of the node, or undefined when no title
  10266. * has been set.
  10267. */
  10268. Node.prototype.getTitle = function() {
  10269. return this.title;
  10270. };
  10271. /**
  10272. * Calculate the distance to the border of the Node
  10273. * @param {CanvasRenderingContext2D} ctx
  10274. * @param {Number} angle Angle in radians
  10275. * @returns {number} distance Distance to the border in pixels
  10276. */
  10277. Node.prototype.distanceToBorder = function (ctx, angle) {
  10278. var borderWidth = 1;
  10279. if (!this.width) {
  10280. this.resize(ctx);
  10281. }
  10282. //noinspection FallthroughInSwitchStatementJS
  10283. switch (this.shape) {
  10284. case 'circle':
  10285. case 'dot':
  10286. return this.radius + borderWidth;
  10287. case 'ellipse':
  10288. var a = this.width / 2;
  10289. var b = this.height / 2;
  10290. var w = (Math.sin(angle) * a);
  10291. var h = (Math.cos(angle) * b);
  10292. return a * b / Math.sqrt(w * w + h * h);
  10293. // TODO: implement distanceToBorder for database
  10294. // TODO: implement distanceToBorder for triangle
  10295. // TODO: implement distanceToBorder for triangleDown
  10296. case 'box':
  10297. case 'image':
  10298. case 'text':
  10299. default:
  10300. if (this.width) {
  10301. return Math.min(
  10302. Math.abs(this.width / 2 / Math.cos(angle)),
  10303. Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
  10304. // TODO: reckon with border radius too in case of box
  10305. }
  10306. else {
  10307. return 0;
  10308. }
  10309. }
  10310. // TODO: implement calculation of distance to border for all shapes
  10311. };
  10312. /**
  10313. * Set forces acting on the node
  10314. * @param {number} fx Force in horizontal direction
  10315. * @param {number} fy Force in vertical direction
  10316. */
  10317. Node.prototype._setForce = function(fx, fy) {
  10318. this.fx = fx;
  10319. this.fy = fy;
  10320. };
  10321. /**
  10322. * Add forces acting on the node
  10323. * @param {number} fx Force in horizontal direction
  10324. * @param {number} fy Force in vertical direction
  10325. * @private
  10326. */
  10327. Node.prototype._addForce = function(fx, fy) {
  10328. this.fx += fx;
  10329. this.fy += fy;
  10330. };
  10331. /**
  10332. * Perform one discrete step for the node
  10333. * @param {number} interval Time interval in seconds
  10334. */
  10335. Node.prototype.discreteStep = function(interval) {
  10336. if (!this.xFixed) {
  10337. var dx = -this.damping * this.vx; // damping force
  10338. var ax = (this.fx + dx) / this.mass; // acceleration
  10339. this.vx += ax / interval; // velocity
  10340. this.x += this.vx / interval; // position
  10341. }
  10342. if (!this.yFixed) {
  10343. var dy = -this.damping * this.vy; // damping force
  10344. var ay = (this.fy + dy) / this.mass; // acceleration
  10345. this.vy += ay / interval; // velocity
  10346. this.y += this.vy / interval; // position
  10347. }
  10348. };
  10349. /**
  10350. * Check if this node has a fixed x and y position
  10351. * @return {boolean} true if fixed, false if not
  10352. */
  10353. Node.prototype.isFixed = function() {
  10354. return (this.xFixed && this.yFixed);
  10355. };
  10356. /**
  10357. * Check if this node is moving
  10358. * @param {number} vmin the minimum velocity considered as "moving"
  10359. * @return {boolean} true if moving, false if it has no velocity
  10360. */
  10361. // TODO: replace this method with calculating the kinetic energy
  10362. Node.prototype.isMoving = function(vmin) {
  10363. return (Math.abs(this.vx) > vmin || Math.abs(this.vy) > vmin ||
  10364. (!this.xFixed && Math.abs(this.fx) > this.minForce) ||
  10365. (!this.yFixed && Math.abs(this.fy) > this.minForce));
  10366. };
  10367. /**
  10368. * check if this node is selecte
  10369. * @return {boolean} selected True if node is selected, else false
  10370. */
  10371. Node.prototype.isSelected = function() {
  10372. return this.selected;
  10373. };
  10374. /**
  10375. * Retrieve the value of the node. Can be undefined
  10376. * @return {Number} value
  10377. */
  10378. Node.prototype.getValue = function() {
  10379. return this.value;
  10380. };
  10381. /**
  10382. * Calculate the distance from the nodes location to the given location (x,y)
  10383. * @param {Number} x
  10384. * @param {Number} y
  10385. * @return {Number} value
  10386. */
  10387. Node.prototype.getDistance = function(x, y) {
  10388. var dx = this.x - x,
  10389. dy = this.y - y;
  10390. return Math.sqrt(dx * dx + dy * dy);
  10391. };
  10392. /**
  10393. * Adjust the value range of the node. The node will adjust it's radius
  10394. * based on its value.
  10395. * @param {Number} min
  10396. * @param {Number} max
  10397. */
  10398. Node.prototype.setValueRange = function(min, max) {
  10399. if (!this.radiusFixed && this.value !== undefined) {
  10400. var scale = (this.radiusMax - this.radiusMin) / (max - min);
  10401. this.radius = (this.value - min) * scale + this.radiusMin;
  10402. }
  10403. };
  10404. /**
  10405. * Draw this node in the given canvas
  10406. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10407. * @param {CanvasRenderingContext2D} ctx
  10408. */
  10409. Node.prototype.draw = function(ctx) {
  10410. throw "Draw method not initialized for node";
  10411. };
  10412. /**
  10413. * Recalculate the size of this node in the given canvas
  10414. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10415. * @param {CanvasRenderingContext2D} ctx
  10416. */
  10417. Node.prototype.resize = function(ctx) {
  10418. throw "Resize method not initialized for node";
  10419. };
  10420. /**
  10421. * Check if this object is overlapping with the provided object
  10422. * @param {Object} obj an object with parameters left, top, right, bottom
  10423. * @return {boolean} True if location is located on node
  10424. */
  10425. Node.prototype.isOverlappingWith = function(obj) {
  10426. return (this.left < obj.right &&
  10427. this.left + this.width > obj.left &&
  10428. this.top < obj.bottom &&
  10429. this.top + this.height > obj.top);
  10430. };
  10431. Node.prototype._resizeImage = function (ctx) {
  10432. // TODO: pre calculate the image size
  10433. if (!this.width) { // undefined or 0
  10434. var width, height;
  10435. if (this.value) {
  10436. var scale = this.imageObj.height / this.imageObj.width;
  10437. width = this.radius || this.imageObj.width;
  10438. height = this.radius * scale || this.imageObj.height;
  10439. }
  10440. else {
  10441. width = this.imageObj.width;
  10442. height = this.imageObj.height;
  10443. }
  10444. this.width = width;
  10445. this.height = height;
  10446. }
  10447. };
  10448. Node.prototype._drawImage = function (ctx) {
  10449. this._resizeImage(ctx);
  10450. this.left = this.x - this.width / 2;
  10451. this.top = this.y - this.height / 2;
  10452. var yLabel;
  10453. if (this.imageObj) {
  10454. ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
  10455. yLabel = this.y + this.height / 2;
  10456. }
  10457. else {
  10458. // image still loading... just draw the label for now
  10459. yLabel = this.y;
  10460. }
  10461. this._label(ctx, this.label, this.x, yLabel, undefined, "top");
  10462. };
  10463. Node.prototype._resizeBox = function (ctx) {
  10464. if (!this.width) {
  10465. var margin = 5;
  10466. var textSize = this.getTextSize(ctx);
  10467. this.width = textSize.width + 2 * margin;
  10468. this.height = textSize.height + 2 * margin;
  10469. }
  10470. };
  10471. Node.prototype._drawBox = function (ctx) {
  10472. this._resizeBox(ctx);
  10473. this.left = this.x - this.width / 2;
  10474. this.top = this.y - this.height / 2;
  10475. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  10476. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  10477. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  10478. ctx.roundRect(this.left, this.top, this.width, this.height, this.radius);
  10479. ctx.fill();
  10480. ctx.stroke();
  10481. this._label(ctx, this.label, this.x, this.y);
  10482. };
  10483. Node.prototype._resizeDatabase = function (ctx) {
  10484. if (!this.width) {
  10485. var margin = 5;
  10486. var textSize = this.getTextSize(ctx);
  10487. var size = textSize.width + 2 * margin;
  10488. this.width = size;
  10489. this.height = size;
  10490. }
  10491. };
  10492. Node.prototype._drawDatabase = function (ctx) {
  10493. this._resizeDatabase(ctx);
  10494. this.left = this.x - this.width / 2;
  10495. this.top = this.y - this.height / 2;
  10496. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  10497. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  10498. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  10499. ctx.database(this.x - this.width/2, this.y - this.height*0.5, this.width, this.height);
  10500. ctx.fill();
  10501. ctx.stroke();
  10502. this._label(ctx, this.label, this.x, this.y);
  10503. };
  10504. Node.prototype._resizeCircle = function (ctx) {
  10505. if (!this.width) {
  10506. var margin = 5;
  10507. var textSize = this.getTextSize(ctx);
  10508. var diameter = Math.max(textSize.width, textSize.height) + 2 * margin;
  10509. this.radius = diameter / 2;
  10510. this.width = diameter;
  10511. this.height = diameter;
  10512. }
  10513. };
  10514. Node.prototype._drawCircle = function (ctx) {
  10515. this._resizeCircle(ctx);
  10516. this.left = this.x - this.width / 2;
  10517. this.top = this.y - this.height / 2;
  10518. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  10519. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  10520. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  10521. ctx.circle(this.x, this.y, this.radius);
  10522. ctx.fill();
  10523. ctx.stroke();
  10524. this._label(ctx, this.label, this.x, this.y);
  10525. };
  10526. Node.prototype._resizeEllipse = function (ctx) {
  10527. if (!this.width) {
  10528. var textSize = this.getTextSize(ctx);
  10529. this.width = textSize.width * 1.5;
  10530. this.height = textSize.height * 2;
  10531. if (this.width < this.height) {
  10532. this.width = this.height;
  10533. }
  10534. }
  10535. };
  10536. Node.prototype._drawEllipse = function (ctx) {
  10537. this._resizeEllipse(ctx);
  10538. this.left = this.x - this.width / 2;
  10539. this.top = this.y - this.height / 2;
  10540. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  10541. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  10542. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  10543. ctx.ellipse(this.left, this.top, this.width, this.height);
  10544. ctx.fill();
  10545. ctx.stroke();
  10546. this._label(ctx, this.label, this.x, this.y);
  10547. };
  10548. Node.prototype._drawDot = function (ctx) {
  10549. this._drawShape(ctx, 'circle');
  10550. };
  10551. Node.prototype._drawTriangle = function (ctx) {
  10552. this._drawShape(ctx, 'triangle');
  10553. };
  10554. Node.prototype._drawTriangleDown = function (ctx) {
  10555. this._drawShape(ctx, 'triangleDown');
  10556. };
  10557. Node.prototype._drawSquare = function (ctx) {
  10558. this._drawShape(ctx, 'square');
  10559. };
  10560. Node.prototype._drawStar = function (ctx) {
  10561. this._drawShape(ctx, 'star');
  10562. };
  10563. Node.prototype._resizeShape = function (ctx) {
  10564. if (!this.width) {
  10565. var size = 2 * this.radius;
  10566. this.width = size;
  10567. this.height = size;
  10568. }
  10569. };
  10570. Node.prototype._drawShape = function (ctx, shape) {
  10571. this._resizeShape(ctx);
  10572. this.left = this.x - this.width / 2;
  10573. this.top = this.y - this.height / 2;
  10574. ctx.strokeStyle = this.selected ? this.color.highlight.border : this.color.border;
  10575. ctx.fillStyle = this.selected ? this.color.highlight.background : this.color.background;
  10576. ctx.lineWidth = this.selected ? 2.0 : 1.0;
  10577. ctx[shape](this.x, this.y, this.radius);
  10578. ctx.fill();
  10579. ctx.stroke();
  10580. if (this.label) {
  10581. this._label(ctx, this.label, this.x, this.y + this.height / 2, undefined, 'top');
  10582. }
  10583. };
  10584. Node.prototype._resizeText = function (ctx) {
  10585. if (!this.width) {
  10586. var margin = 5;
  10587. var textSize = this.getTextSize(ctx);
  10588. this.width = textSize.width + 2 * margin;
  10589. this.height = textSize.height + 2 * margin;
  10590. }
  10591. };
  10592. Node.prototype._drawText = function (ctx) {
  10593. this._resizeText(ctx);
  10594. this.left = this.x - this.width / 2;
  10595. this.top = this.y - this.height / 2;
  10596. this._label(ctx, this.label, this.x, this.y);
  10597. };
  10598. Node.prototype._label = function (ctx, text, x, y, align, baseline) {
  10599. if (text) {
  10600. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  10601. ctx.fillStyle = this.fontColor || "black";
  10602. ctx.textAlign = align || "center";
  10603. ctx.textBaseline = baseline || "middle";
  10604. var lines = text.split('\n'),
  10605. lineCount = lines.length,
  10606. fontSize = (this.fontSize + 4),
  10607. yLine = y + (1 - lineCount) / 2 * fontSize;
  10608. for (var i = 0; i < lineCount; i++) {
  10609. ctx.fillText(lines[i], x, yLine);
  10610. yLine += fontSize;
  10611. }
  10612. }
  10613. };
  10614. Node.prototype.getTextSize = function(ctx) {
  10615. if (this.label != undefined) {
  10616. ctx.font = (this.selected ? "bold " : "") + this.fontSize + "px " + this.fontFace;
  10617. var lines = this.label.split('\n'),
  10618. height = (this.fontSize + 4) * lines.length,
  10619. width = 0;
  10620. for (var i = 0, iMax = lines.length; i < iMax; i++) {
  10621. width = Math.max(width, ctx.measureText(lines[i]).width);
  10622. }
  10623. return {"width": width, "height": height};
  10624. }
  10625. else {
  10626. return {"width": 0, "height": 0};
  10627. }
  10628. };
  10629. /**
  10630. * @class Edge
  10631. *
  10632. * A edge connects two nodes
  10633. * @param {Object} properties Object with properties. Must contain
  10634. * At least properties from and to.
  10635. * Available properties: from (number),
  10636. * to (number), label (string, color (string),
  10637. * width (number), style (string),
  10638. * length (number), title (string)
  10639. * @param {Graph} graph A graph object, used to find and edge to
  10640. * nodes.
  10641. * @param {Object} constants An object with default values for
  10642. * example for the color
  10643. */
  10644. function Edge (properties, graph, constants) {
  10645. if (!graph) {
  10646. throw "No graph provided";
  10647. }
  10648. this.graph = graph;
  10649. // initialize constants
  10650. this.widthMin = constants.edges.widthMin;
  10651. this.widthMax = constants.edges.widthMax;
  10652. // initialize variables
  10653. this.id = undefined;
  10654. this.fromId = undefined;
  10655. this.toId = undefined;
  10656. this.style = constants.edges.style;
  10657. this.title = undefined;
  10658. this.width = constants.edges.width;
  10659. this.value = undefined;
  10660. this.length = constants.edges.length;
  10661. this.from = null; // a node
  10662. this.to = null; // a node
  10663. this.connected = false;
  10664. // Added to support dashed lines
  10665. // David Jordan
  10666. // 2012-08-08
  10667. this.dash = util.extend({}, constants.edges.dash); // contains properties length, gap, altLength
  10668. this.stiffness = undefined; // depends on the length of the edge
  10669. this.color = constants.edges.color;
  10670. this.widthFixed = false;
  10671. this.lengthFixed = false;
  10672. this.setProperties(properties, constants);
  10673. }
  10674. /**
  10675. * Set or overwrite properties for the edge
  10676. * @param {Object} properties an object with properties
  10677. * @param {Object} constants and object with default, global properties
  10678. */
  10679. Edge.prototype.setProperties = function(properties, constants) {
  10680. if (!properties) {
  10681. return;
  10682. }
  10683. if (properties.from != undefined) {this.fromId = properties.from;}
  10684. if (properties.to != undefined) {this.toId = properties.to;}
  10685. if (properties.id != undefined) {this.id = properties.id;}
  10686. if (properties.style != undefined) {this.style = properties.style;}
  10687. if (properties.label != undefined) {this.label = properties.label;}
  10688. if (this.label) {
  10689. this.fontSize = constants.edges.fontSize;
  10690. this.fontFace = constants.edges.fontFace;
  10691. this.fontColor = constants.edges.fontColor;
  10692. if (properties.fontColor != undefined) {this.fontColor = properties.fontColor;}
  10693. if (properties.fontSize != undefined) {this.fontSize = properties.fontSize;}
  10694. if (properties.fontFace != undefined) {this.fontFace = properties.fontFace;}
  10695. }
  10696. if (properties.title != undefined) {this.title = properties.title;}
  10697. if (properties.width != undefined) {this.width = properties.width;}
  10698. if (properties.value != undefined) {this.value = properties.value;}
  10699. if (properties.length != undefined) {this.length = properties.length;}
  10700. // Added to support dashed lines
  10701. // David Jordan
  10702. // 2012-08-08
  10703. if (properties.dash) {
  10704. if (properties.dash.length != undefined) {this.dash.length = properties.dash.length;}
  10705. if (properties.dash.gap != undefined) {this.dash.gap = properties.dash.gap;}
  10706. if (properties.dash.altLength != undefined) {this.dash.altLength = properties.dash.altLength;}
  10707. }
  10708. if (properties.color != undefined) {this.color = properties.color;}
  10709. // A node is connected when it has a from and to node.
  10710. this.connect();
  10711. this.widthFixed = this.widthFixed || (properties.width != undefined);
  10712. this.lengthFixed = this.lengthFixed || (properties.length != undefined);
  10713. this.stiffness = 1 / this.length;
  10714. // set draw method based on style
  10715. switch (this.style) {
  10716. case 'line': this.draw = this._drawLine; break;
  10717. case 'arrow': this.draw = this._drawArrow; break;
  10718. case 'arrow-center': this.draw = this._drawArrowCenter; break;
  10719. case 'dash-line': this.draw = this._drawDashLine; break;
  10720. default: this.draw = this._drawLine; break;
  10721. }
  10722. };
  10723. /**
  10724. * Connect an edge to its nodes
  10725. */
  10726. Edge.prototype.connect = function () {
  10727. this.disconnect();
  10728. this.from = this.graph.nodes[this.fromId] || null;
  10729. this.to = this.graph.nodes[this.toId] || null;
  10730. this.connected = (this.from && this.to);
  10731. if (this.connected) {
  10732. this.from.attachEdge(this);
  10733. this.to.attachEdge(this);
  10734. }
  10735. else {
  10736. if (this.from) {
  10737. this.from.detachEdge(this);
  10738. }
  10739. if (this.to) {
  10740. this.to.detachEdge(this);
  10741. }
  10742. }
  10743. };
  10744. /**
  10745. * Disconnect an edge from its nodes
  10746. */
  10747. Edge.prototype.disconnect = function () {
  10748. if (this.from) {
  10749. this.from.detachEdge(this);
  10750. this.from = null;
  10751. }
  10752. if (this.to) {
  10753. this.to.detachEdge(this);
  10754. this.to = null;
  10755. }
  10756. this.connected = false;
  10757. };
  10758. /**
  10759. * get the title of this edge.
  10760. * @return {string} title The title of the edge, or undefined when no title
  10761. * has been set.
  10762. */
  10763. Edge.prototype.getTitle = function() {
  10764. return this.title;
  10765. };
  10766. /**
  10767. * Retrieve the value of the edge. Can be undefined
  10768. * @return {Number} value
  10769. */
  10770. Edge.prototype.getValue = function() {
  10771. return this.value;
  10772. };
  10773. /**
  10774. * Adjust the value range of the edge. The edge will adjust it's width
  10775. * based on its value.
  10776. * @param {Number} min
  10777. * @param {Number} max
  10778. */
  10779. Edge.prototype.setValueRange = function(min, max) {
  10780. if (!this.widthFixed && this.value !== undefined) {
  10781. var factor = (this.widthMax - this.widthMin) / (max - min);
  10782. this.width = (this.value - min) * factor + this.widthMin;
  10783. }
  10784. };
  10785. /**
  10786. * Redraw a edge
  10787. * Draw this edge in the given canvas
  10788. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10789. * @param {CanvasRenderingContext2D} ctx
  10790. */
  10791. Edge.prototype.draw = function(ctx) {
  10792. throw "Method draw not initialized in edge";
  10793. };
  10794. /**
  10795. * Check if this object is overlapping with the provided object
  10796. * @param {Object} obj an object with parameters left, top
  10797. * @return {boolean} True if location is located on the edge
  10798. */
  10799. Edge.prototype.isOverlappingWith = function(obj) {
  10800. var distMax = 10;
  10801. var xFrom = this.from.x;
  10802. var yFrom = this.from.y;
  10803. var xTo = this.to.x;
  10804. var yTo = this.to.y;
  10805. var xObj = obj.left;
  10806. var yObj = obj.top;
  10807. var dist = Edge._dist(xFrom, yFrom, xTo, yTo, xObj, yObj);
  10808. return (dist < distMax);
  10809. };
  10810. /**
  10811. * Redraw a edge as a line
  10812. * Draw this edge in the given canvas
  10813. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10814. * @param {CanvasRenderingContext2D} ctx
  10815. * @private
  10816. */
  10817. Edge.prototype._drawLine = function(ctx) {
  10818. // set style
  10819. ctx.strokeStyle = this.color;
  10820. ctx.lineWidth = this._getLineWidth();
  10821. var point;
  10822. if (this.from != this.to) {
  10823. // draw line
  10824. this._line(ctx);
  10825. // draw label
  10826. if (this.label) {
  10827. point = this._pointOnLine(0.5);
  10828. this._label(ctx, this.label, point.x, point.y);
  10829. }
  10830. }
  10831. else {
  10832. var x, y;
  10833. var radius = this.length / 4;
  10834. var node = this.from;
  10835. if (!node.width) {
  10836. node.resize(ctx);
  10837. }
  10838. if (node.width > node.height) {
  10839. x = node.x + node.width / 2;
  10840. y = node.y - radius;
  10841. }
  10842. else {
  10843. x = node.x + radius;
  10844. y = node.y - node.height / 2;
  10845. }
  10846. this._circle(ctx, x, y, radius);
  10847. point = this._pointOnCircle(x, y, radius, 0.5);
  10848. this._label(ctx, this.label, point.x, point.y);
  10849. }
  10850. };
  10851. /**
  10852. * Get the line width of the edge. Depends on width and whether one of the
  10853. * connected nodes is selected.
  10854. * @return {Number} width
  10855. * @private
  10856. */
  10857. Edge.prototype._getLineWidth = function() {
  10858. if (this.from.selected || this.to.selected) {
  10859. return Math.min(this.width * 2, this.widthMax);
  10860. }
  10861. else {
  10862. return this.width;
  10863. }
  10864. };
  10865. /**
  10866. * Draw a line between two nodes
  10867. * @param {CanvasRenderingContext2D} ctx
  10868. * @private
  10869. */
  10870. Edge.prototype._line = function (ctx) {
  10871. // draw a straight line
  10872. ctx.beginPath();
  10873. ctx.moveTo(this.from.x, this.from.y);
  10874. ctx.lineTo(this.to.x, this.to.y);
  10875. ctx.stroke();
  10876. };
  10877. /**
  10878. * Draw a line from a node to itself, a circle
  10879. * @param {CanvasRenderingContext2D} ctx
  10880. * @param {Number} x
  10881. * @param {Number} y
  10882. * @param {Number} radius
  10883. * @private
  10884. */
  10885. Edge.prototype._circle = function (ctx, x, y, radius) {
  10886. // draw a circle
  10887. ctx.beginPath();
  10888. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  10889. ctx.stroke();
  10890. };
  10891. /**
  10892. * Draw label with white background and with the middle at (x, y)
  10893. * @param {CanvasRenderingContext2D} ctx
  10894. * @param {String} text
  10895. * @param {Number} x
  10896. * @param {Number} y
  10897. * @private
  10898. */
  10899. Edge.prototype._label = function (ctx, text, x, y) {
  10900. if (text) {
  10901. // TODO: cache the calculated size
  10902. ctx.font = ((this.from.selected || this.to.selected) ? "bold " : "") +
  10903. this.fontSize + "px " + this.fontFace;
  10904. ctx.fillStyle = 'white';
  10905. var width = ctx.measureText(text).width;
  10906. var height = this.fontSize;
  10907. var left = x - width / 2;
  10908. var top = y - height / 2;
  10909. ctx.fillRect(left, top, width, height);
  10910. // draw text
  10911. ctx.fillStyle = this.fontColor || "black";
  10912. ctx.textAlign = "left";
  10913. ctx.textBaseline = "top";
  10914. ctx.fillText(text, left, top);
  10915. }
  10916. };
  10917. /**
  10918. * Redraw a edge as a dashed line
  10919. * Draw this edge in the given canvas
  10920. * @author David Jordan
  10921. * @date 2012-08-08
  10922. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10923. * @param {CanvasRenderingContext2D} ctx
  10924. * @private
  10925. */
  10926. Edge.prototype._drawDashLine = function(ctx) {
  10927. // set style
  10928. ctx.strokeStyle = this.color;
  10929. ctx.lineWidth = this._getLineWidth();
  10930. // draw dashed line
  10931. ctx.beginPath();
  10932. ctx.lineCap = 'round';
  10933. if (this.dash.altLength != undefined) //If an alt dash value has been set add to the array this value
  10934. {
  10935. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  10936. [this.dash.length,this.dash.gap,this.dash.altLength,this.dash.gap]);
  10937. }
  10938. 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
  10939. {
  10940. ctx.dashedLine(this.from.x,this.from.y,this.to.x,this.to.y,
  10941. [this.dash.length,this.dash.gap]);
  10942. }
  10943. else //If all else fails draw a line
  10944. {
  10945. ctx.moveTo(this.from.x, this.from.y);
  10946. ctx.lineTo(this.to.x, this.to.y);
  10947. }
  10948. ctx.stroke();
  10949. // draw label
  10950. if (this.label) {
  10951. var point = this._pointOnLine(0.5);
  10952. this._label(ctx, this.label, point.x, point.y);
  10953. }
  10954. };
  10955. /**
  10956. * Get a point on a line
  10957. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  10958. * @return {Object} point
  10959. * @private
  10960. */
  10961. Edge.prototype._pointOnLine = function (percentage) {
  10962. return {
  10963. x: (1 - percentage) * this.from.x + percentage * this.to.x,
  10964. y: (1 - percentage) * this.from.y + percentage * this.to.y
  10965. }
  10966. };
  10967. /**
  10968. * Get a point on a circle
  10969. * @param {Number} x
  10970. * @param {Number} y
  10971. * @param {Number} radius
  10972. * @param {Number} percentage. Value between 0 (line start) and 1 (line end)
  10973. * @return {Object} point
  10974. * @private
  10975. */
  10976. Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
  10977. var angle = (percentage - 3/8) * 2 * Math.PI;
  10978. return {
  10979. x: x + radius * Math.cos(angle),
  10980. y: y - radius * Math.sin(angle)
  10981. }
  10982. };
  10983. /**
  10984. * Redraw a edge as a line with an arrow halfway the line
  10985. * Draw this edge in the given canvas
  10986. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  10987. * @param {CanvasRenderingContext2D} ctx
  10988. * @private
  10989. */
  10990. Edge.prototype._drawArrowCenter = function(ctx) {
  10991. var point;
  10992. // set style
  10993. ctx.strokeStyle = this.color;
  10994. ctx.fillStyle = this.color;
  10995. ctx.lineWidth = this._getLineWidth();
  10996. if (this.from != this.to) {
  10997. // draw line
  10998. this._line(ctx);
  10999. // draw an arrow halfway the line
  11000. var angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  11001. var length = 10 + 5 * this.width; // TODO: make customizable?
  11002. point = this._pointOnLine(0.5);
  11003. ctx.arrow(point.x, point.y, angle, length);
  11004. ctx.fill();
  11005. ctx.stroke();
  11006. // draw label
  11007. if (this.label) {
  11008. point = this._pointOnLine(0.5);
  11009. this._label(ctx, this.label, point.x, point.y);
  11010. }
  11011. }
  11012. else {
  11013. // draw circle
  11014. var x, y;
  11015. var radius = this.length / 4;
  11016. var node = this.from;
  11017. if (!node.width) {
  11018. node.resize(ctx);
  11019. }
  11020. if (node.width > node.height) {
  11021. x = node.x + node.width / 2;
  11022. y = node.y - radius;
  11023. }
  11024. else {
  11025. x = node.x + radius;
  11026. y = node.y - node.height / 2;
  11027. }
  11028. this._circle(ctx, x, y, radius);
  11029. // draw all arrows
  11030. var angle = 0.2 * Math.PI;
  11031. var length = 10 + 5 * this.width; // TODO: make customizable?
  11032. point = this._pointOnCircle(x, y, radius, 0.5);
  11033. ctx.arrow(point.x, point.y, angle, length);
  11034. ctx.fill();
  11035. ctx.stroke();
  11036. // draw label
  11037. if (this.label) {
  11038. point = this._pointOnCircle(x, y, radius, 0.5);
  11039. this._label(ctx, this.label, point.x, point.y);
  11040. }
  11041. }
  11042. };
  11043. /**
  11044. * Redraw a edge as a line with an arrow
  11045. * Draw this edge in the given canvas
  11046. * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
  11047. * @param {CanvasRenderingContext2D} ctx
  11048. * @private
  11049. */
  11050. Edge.prototype._drawArrow = function(ctx) {
  11051. // set style
  11052. ctx.strokeStyle = this.color;
  11053. ctx.fillStyle = this.color;
  11054. ctx.lineWidth = this._getLineWidth();
  11055. // draw line
  11056. var angle, length;
  11057. if (this.from != this.to) {
  11058. // calculate length and angle of the line
  11059. angle = Math.atan2((this.to.y - this.from.y), (this.to.x - this.from.x));
  11060. var dx = (this.to.x - this.from.x);
  11061. var dy = (this.to.y - this.from.y);
  11062. var lEdge = Math.sqrt(dx * dx + dy * dy);
  11063. var lFrom = this.from.distanceToBorder(ctx, angle + Math.PI);
  11064. var pFrom = (lEdge - lFrom) / lEdge;
  11065. var xFrom = (pFrom) * this.from.x + (1 - pFrom) * this.to.x;
  11066. var yFrom = (pFrom) * this.from.y + (1 - pFrom) * this.to.y;
  11067. var lTo = this.to.distanceToBorder(ctx, angle);
  11068. var pTo = (lEdge - lTo) / lEdge;
  11069. var xTo = (1 - pTo) * this.from.x + pTo * this.to.x;
  11070. var yTo = (1 - pTo) * this.from.y + pTo * this.to.y;
  11071. ctx.beginPath();
  11072. ctx.moveTo(xFrom, yFrom);
  11073. ctx.lineTo(xTo, yTo);
  11074. ctx.stroke();
  11075. // draw arrow at the end of the line
  11076. length = 10 + 5 * this.width; // TODO: make customizable?
  11077. ctx.arrow(xTo, yTo, angle, length);
  11078. ctx.fill();
  11079. ctx.stroke();
  11080. // draw label
  11081. if (this.label) {
  11082. var point = this._pointOnLine(0.5);
  11083. this._label(ctx, this.label, point.x, point.y);
  11084. }
  11085. }
  11086. else {
  11087. // draw circle
  11088. var node = this.from;
  11089. var x, y, arrow;
  11090. var radius = this.length / 4;
  11091. if (!node.width) {
  11092. node.resize(ctx);
  11093. }
  11094. if (node.width > node.height) {
  11095. x = node.x + node.width / 2;
  11096. y = node.y - radius;
  11097. arrow = {
  11098. x: x,
  11099. y: node.y,
  11100. angle: 0.9 * Math.PI
  11101. };
  11102. }
  11103. else {
  11104. x = node.x + radius;
  11105. y = node.y - node.height / 2;
  11106. arrow = {
  11107. x: node.x,
  11108. y: y,
  11109. angle: 0.6 * Math.PI
  11110. };
  11111. }
  11112. ctx.beginPath();
  11113. // TODO: do not draw a circle, but an arc
  11114. // TODO: similarly, for a line without arrows, draw to the border of the nodes instead of the center
  11115. ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
  11116. ctx.stroke();
  11117. // draw all arrows
  11118. length = 10 + 5 * this.width; // TODO: make customizable?
  11119. ctx.arrow(arrow.x, arrow.y, arrow.angle, length);
  11120. ctx.fill();
  11121. ctx.stroke();
  11122. // draw label
  11123. if (this.label) {
  11124. point = this._pointOnCircle(x, y, radius, 0.5);
  11125. this._label(ctx, this.label, point.x, point.y);
  11126. }
  11127. }
  11128. };
  11129. /**
  11130. * Calculate the distance between a point (x3,y3) and a line segment from
  11131. * (x1,y1) to (x2,y2).
  11132. * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
  11133. * @param {number} x1
  11134. * @param {number} y1
  11135. * @param {number} x2
  11136. * @param {number} y2
  11137. * @param {number} x3
  11138. * @param {number} y3
  11139. * @private
  11140. */
  11141. Edge._dist = function (x1,y1, x2,y2, x3,y3) { // x3,y3 is the point
  11142. var px = x2-x1,
  11143. py = y2-y1,
  11144. something = px*px + py*py,
  11145. u = ((x3 - x1) * px + (y3 - y1) * py) / something;
  11146. if (u > 1) {
  11147. u = 1;
  11148. }
  11149. else if (u < 0) {
  11150. u = 0;
  11151. }
  11152. var x = x1 + u * px,
  11153. y = y1 + u * py,
  11154. dx = x - x3,
  11155. dy = y - y3;
  11156. //# Note: If the actual distance does not matter,
  11157. //# if you only want to compare what this function
  11158. //# returns to other results of this function, you
  11159. //# can just return the squared distance instead
  11160. //# (i.e. remove the sqrt) to gain a little performance
  11161. return Math.sqrt(dx*dx + dy*dy);
  11162. };
  11163. /**
  11164. * Popup is a class to create a popup window with some text
  11165. * @param {Element} container The container object.
  11166. * @param {Number} [x]
  11167. * @param {Number} [y]
  11168. * @param {String} [text]
  11169. */
  11170. function Popup(container, x, y, text) {
  11171. if (container) {
  11172. this.container = container;
  11173. }
  11174. else {
  11175. this.container = document.body;
  11176. }
  11177. this.x = 0;
  11178. this.y = 0;
  11179. this.padding = 5;
  11180. if (x !== undefined && y !== undefined ) {
  11181. this.setPosition(x, y);
  11182. }
  11183. if (text !== undefined) {
  11184. this.setText(text);
  11185. }
  11186. // create the frame
  11187. this.frame = document.createElement("div");
  11188. var style = this.frame.style;
  11189. style.position = "absolute";
  11190. style.visibility = "hidden";
  11191. style.border = "1px solid #666";
  11192. style.color = "black";
  11193. style.padding = this.padding + "px";
  11194. style.backgroundColor = "#FFFFC6";
  11195. style.borderRadius = "3px";
  11196. style.MozBorderRadius = "3px";
  11197. style.WebkitBorderRadius = "3px";
  11198. style.boxShadow = "3px 3px 10px rgba(128, 128, 128, 0.5)";
  11199. style.whiteSpace = "nowrap";
  11200. this.container.appendChild(this.frame);
  11201. };
  11202. /**
  11203. * @param {number} x Horizontal position of the popup window
  11204. * @param {number} y Vertical position of the popup window
  11205. */
  11206. Popup.prototype.setPosition = function(x, y) {
  11207. this.x = parseInt(x);
  11208. this.y = parseInt(y);
  11209. };
  11210. /**
  11211. * Set the text for the popup window. This can be HTML code
  11212. * @param {string} text
  11213. */
  11214. Popup.prototype.setText = function(text) {
  11215. this.frame.innerHTML = text;
  11216. };
  11217. /**
  11218. * Show the popup window
  11219. * @param {boolean} show Optional. Show or hide the window
  11220. */
  11221. Popup.prototype.show = function (show) {
  11222. if (show === undefined) {
  11223. show = true;
  11224. }
  11225. if (show) {
  11226. var height = this.frame.clientHeight;
  11227. var width = this.frame.clientWidth;
  11228. var maxHeight = this.frame.parentNode.clientHeight;
  11229. var maxWidth = this.frame.parentNode.clientWidth;
  11230. var top = (this.y - height);
  11231. if (top + height + this.padding > maxHeight) {
  11232. top = maxHeight - height - this.padding;
  11233. }
  11234. if (top < this.padding) {
  11235. top = this.padding;
  11236. }
  11237. var left = this.x;
  11238. if (left + width + this.padding > maxWidth) {
  11239. left = maxWidth - width - this.padding;
  11240. }
  11241. if (left < this.padding) {
  11242. left = this.padding;
  11243. }
  11244. this.frame.style.left = left + "px";
  11245. this.frame.style.top = top + "px";
  11246. this.frame.style.visibility = "visible";
  11247. }
  11248. else {
  11249. this.hide();
  11250. }
  11251. };
  11252. /**
  11253. * Hide the popup window
  11254. */
  11255. Popup.prototype.hide = function () {
  11256. this.frame.style.visibility = "hidden";
  11257. };
  11258. /**
  11259. * @class Groups
  11260. * This class can store groups and properties specific for groups.
  11261. */
  11262. Groups = function () {
  11263. this.clear();
  11264. this.defaultIndex = 0;
  11265. };
  11266. /**
  11267. * default constants for group colors
  11268. */
  11269. Groups.DEFAULT = [
  11270. {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
  11271. {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
  11272. {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}}, // red
  11273. {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}}, // green
  11274. {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
  11275. {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
  11276. {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}}, // orange
  11277. {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
  11278. {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
  11279. {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}} // mint
  11280. ];
  11281. /**
  11282. * Clear all groups
  11283. */
  11284. Groups.prototype.clear = function () {
  11285. this.groups = {};
  11286. this.groups.length = function()
  11287. {
  11288. var i = 0;
  11289. for ( var p in this ) {
  11290. if (this.hasOwnProperty(p)) {
  11291. i++;
  11292. }
  11293. }
  11294. return i;
  11295. }
  11296. };
  11297. /**
  11298. * get group properties of a groupname. If groupname is not found, a new group
  11299. * is added.
  11300. * @param {*} groupname Can be a number, string, Date, etc.
  11301. * @return {Object} group The created group, containing all group properties
  11302. */
  11303. Groups.prototype.get = function (groupname) {
  11304. var group = this.groups[groupname];
  11305. if (group == undefined) {
  11306. // create new group
  11307. var index = this.defaultIndex % Groups.DEFAULT.length;
  11308. this.defaultIndex++;
  11309. group = {};
  11310. group.color = Groups.DEFAULT[index];
  11311. this.groups[groupname] = group;
  11312. }
  11313. return group;
  11314. };
  11315. /**
  11316. * Add a custom group style
  11317. * @param {String} groupname
  11318. * @param {Object} style An object containing borderColor,
  11319. * backgroundColor, etc.
  11320. * @return {Object} group The created group object
  11321. */
  11322. Groups.prototype.add = function (groupname, style) {
  11323. this.groups[groupname] = style;
  11324. if (style.color) {
  11325. style.color = Node.parseColor(style.color);
  11326. }
  11327. return style;
  11328. };
  11329. /**
  11330. * @class Images
  11331. * This class loads images and keeps them stored.
  11332. */
  11333. Images = function () {
  11334. this.images = {};
  11335. this.callback = undefined;
  11336. };
  11337. /**
  11338. * Set an onload callback function. This will be called each time an image
  11339. * is loaded
  11340. * @param {function} callback
  11341. */
  11342. Images.prototype.setOnloadCallback = function(callback) {
  11343. this.callback = callback;
  11344. };
  11345. /**
  11346. *
  11347. * @param {string} url Url of the image
  11348. * @return {Image} img The image object
  11349. */
  11350. Images.prototype.load = function(url) {
  11351. var img = this.images[url];
  11352. if (img == undefined) {
  11353. // create the image
  11354. var images = this;
  11355. img = new Image();
  11356. this.images[url] = img;
  11357. img.onload = function() {
  11358. if (images.callback) {
  11359. images.callback(this);
  11360. }
  11361. };
  11362. img.src = url;
  11363. }
  11364. return img;
  11365. };
  11366. /**
  11367. * @constructor Graph
  11368. * Create a graph visualization, displaying nodes and edges.
  11369. *
  11370. * @param {Element} container The DOM element in which the Graph will
  11371. * be created. Normally a div element.
  11372. * @param {Object} data An object containing parameters
  11373. * {Array} nodes
  11374. * {Array} edges
  11375. * @param {Object} options Options
  11376. */
  11377. function Graph (container, data, options) {
  11378. // create variables and set default values
  11379. this.containerElement = container;
  11380. this.width = '100%';
  11381. this.height = '100%';
  11382. this.refreshRate = 50; // milliseconds
  11383. this.stabilize = true; // stabilize before displaying the graph
  11384. this.selectable = true;
  11385. // set constant values
  11386. this.constants = {
  11387. nodes: {
  11388. radiusMin: 5,
  11389. radiusMax: 20,
  11390. radius: 5,
  11391. distance: 100, // px
  11392. shape: 'ellipse',
  11393. image: undefined,
  11394. widthMin: 16, // px
  11395. widthMax: 64, // px
  11396. fontColor: 'black',
  11397. fontSize: 14, // px
  11398. //fontFace: verdana,
  11399. fontFace: 'arial',
  11400. color: {
  11401. border: '#2B7CE9',
  11402. background: '#97C2FC',
  11403. highlight: {
  11404. border: '#2B7CE9',
  11405. background: '#D2E5FF'
  11406. }
  11407. },
  11408. borderColor: '#2B7CE9',
  11409. backgroundColor: '#97C2FC',
  11410. highlightColor: '#D2E5FF',
  11411. group: undefined
  11412. },
  11413. edges: {
  11414. widthMin: 1,
  11415. widthMax: 15,
  11416. width: 1,
  11417. style: 'line',
  11418. color: '#343434',
  11419. fontColor: '#343434',
  11420. fontSize: 14, // px
  11421. fontFace: 'arial',
  11422. //distance: 100, //px
  11423. length: 100, // px
  11424. dash: {
  11425. length: 10,
  11426. gap: 5,
  11427. altLength: undefined
  11428. }
  11429. },
  11430. minForce: 0.05,
  11431. minVelocity: 0.02, // px/s
  11432. maxIterations: 1000 // maximum number of iteration to stabilize
  11433. };
  11434. var graph = this;
  11435. this.nodes = {}; // object with Node objects
  11436. this.edges = {}; // object with Edge objects
  11437. // TODO: create a counter to keep track on the number of nodes having values
  11438. // TODO: create a counter to keep track on the number of nodes currently moving
  11439. // TODO: create a counter to keep track on the number of edges having values
  11440. <<<<<<< HEAD
  11441. // inject css
  11442. util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
  11443. =======
  11444. this.nodesData = null; // A DataSet or DataView
  11445. this.edgesData = null; // A DataSet or DataView
  11446. >>>>>>> upstream/develop
  11447. // create event listeners used to subscribe on the DataSets of the nodes and edges
  11448. var me = this;
  11449. this.nodesListeners = {
  11450. 'add': function (event, params) {
  11451. me._addNodes(params.items);
  11452. me.start();
  11453. },
  11454. 'update': function (event, params) {
  11455. me._updateNodes(params.items);
  11456. me.start();
  11457. },
  11458. 'remove': function (event, params) {
  11459. me._removeNodes(params.items);
  11460. me.start();
  11461. }
  11462. };
  11463. this.edgesListeners = {
  11464. 'add': function (event, params) {
  11465. me._addEdges(params.items);
  11466. me.start();
  11467. },
  11468. 'update': function (event, params) {
  11469. me._updateEdges(params.items);
  11470. me.start();
  11471. },
  11472. 'remove': function (event, params) {
  11473. me._removeEdges(params.items);
  11474. me.start();
  11475. }
  11476. };
  11477. this.groups = new Groups(); // object with groups
  11478. this.images = new Images(); // object with images
  11479. this.images.setOnloadCallback(function () {
  11480. graph._redraw();
  11481. });
  11482. // properties of the data
  11483. this.moving = false; // True if any of the nodes have an undefined position
  11484. this.selection = [];
  11485. this.timer = undefined;
  11486. // create a frame and canvas
  11487. this._create();
  11488. // apply options
  11489. this.setOptions(options);
  11490. // draw data
  11491. this.setData(data);
  11492. }
  11493. /**
  11494. * Set nodes and edges, and optionally options as well.
  11495. *
  11496. * @param {Object} data Object containing parameters:
  11497. * {Array | DataSet | DataView} [nodes] Array with nodes
  11498. * {Array | DataSet | DataView} [edges] Array with edges
  11499. * {String} [dot] String containing data in DOT format
  11500. * {Options} [options] Object with options
  11501. */
  11502. Graph.prototype.setData = function(data) {
  11503. if (data && data.dot && (data.nodes || data.edges)) {
  11504. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  11505. ' parameter pair "nodes" and "edges", but not both.');
  11506. }
  11507. // set options
  11508. this.setOptions(data && data.options);
  11509. // set all data
  11510. if (data && data.dot) {
  11511. // parse DOT file
  11512. if(data && data.dot) {
  11513. var dotData = vis.util.DOTToGraph(data.dot);
  11514. this.setData(dotData);
  11515. return;
  11516. }
  11517. }
  11518. else {
  11519. this._setNodes(data && data.nodes);
  11520. this._setEdges(data && data.edges);
  11521. }
  11522. // find a stable position or start animating to a stable position
  11523. if (this.stabilize) {
  11524. this._doStabilize();
  11525. }
  11526. this.start();
  11527. };
  11528. /**
  11529. * Set options
  11530. * @param {Object} options
  11531. */
  11532. Graph.prototype.setOptions = function (options) {
  11533. if (options) {
  11534. // retrieve parameter values
  11535. if (options.width != undefined) {this.width = options.width;}
  11536. if (options.height != undefined) {this.height = options.height;}
  11537. if (options.stabilize != undefined) {this.stabilize = options.stabilize;}
  11538. if (options.selectable != undefined) {this.selectable = options.selectable;}
  11539. // TODO: work out these options and document them
  11540. if (options.edges) {
  11541. for (var prop in options.edges) {
  11542. if (options.edges.hasOwnProperty(prop)) {
  11543. this.constants.edges[prop] = options.edges[prop];
  11544. }
  11545. }
  11546. if (options.edges.length != undefined &&
  11547. options.nodes && options.nodes.distance == undefined) {
  11548. this.constants.edges.length = options.edges.length;
  11549. this.constants.nodes.distance = options.edges.length * 1.25;
  11550. }
  11551. if (!options.edges.fontColor) {
  11552. this.constants.edges.fontColor = options.edges.color;
  11553. }
  11554. // Added to support dashed lines
  11555. // David Jordan
  11556. // 2012-08-08
  11557. if (options.edges.dash) {
  11558. if (options.edges.dash.length != undefined) {
  11559. this.constants.edges.dash.length = options.edges.dash.length;
  11560. }
  11561. if (options.edges.dash.gap != undefined) {
  11562. this.constants.edges.dash.gap = options.edges.dash.gap;
  11563. }
  11564. if (options.edges.dash.altLength != undefined) {
  11565. this.constants.edges.dash.altLength = options.edges.dash.altLength;
  11566. }
  11567. }
  11568. }
  11569. if (options.nodes) {
  11570. for (prop in options.nodes) {
  11571. if (options.nodes.hasOwnProperty(prop)) {
  11572. this.constants.nodes[prop] = options.nodes[prop];
  11573. }
  11574. }
  11575. if (options.nodes.color) {
  11576. this.constants.nodes.color = Node.parseColor(options.nodes.color);
  11577. }
  11578. /*
  11579. if (options.nodes.widthMin) this.constants.nodes.radiusMin = options.nodes.widthMin;
  11580. if (options.nodes.widthMax) this.constants.nodes.radiusMax = options.nodes.widthMax;
  11581. */
  11582. }
  11583. if (options.groups) {
  11584. for (var groupname in options.groups) {
  11585. if (options.groups.hasOwnProperty(groupname)) {
  11586. var group = options.groups[groupname];
  11587. this.groups.add(groupname, group);
  11588. }
  11589. }
  11590. }
  11591. }
  11592. this.setSize(this.width, this.height);
  11593. this._setTranslation(this.frame.clientWidth / 2, this.frame.clientHeight / 2);
  11594. this._setScale(1);
  11595. };
  11596. /**
  11597. * fire an event
  11598. * @param {String} event The name of an event, for example 'select'
  11599. * @param {Object} params Optional object with event parameters
  11600. * @private
  11601. */
  11602. Graph.prototype._trigger = function (event, params) {
  11603. events.trigger(this, event, params);
  11604. };
  11605. /**
  11606. * Create the main frame for the Graph.
  11607. * This function is executed once when a Graph object is created. The frame
  11608. * contains a canvas, and this canvas contains all objects like the axis and
  11609. * nodes.
  11610. * @private
  11611. */
  11612. Graph.prototype._create = function () {
  11613. // remove all elements from the container element.
  11614. while (this.containerElement.hasChildNodes()) {
  11615. this.containerElement.removeChild(this.containerElement.firstChild);
  11616. }
  11617. this.frame = document.createElement('div');
  11618. this.frame.className = 'graph-frame';
  11619. this.frame.style.position = 'relative';
  11620. this.frame.style.overflow = 'hidden';
  11621. // create the graph canvas (HTML canvas element)
  11622. this.frame.canvas = document.createElement( 'canvas' );
  11623. this.frame.canvas.style.position = 'relative';
  11624. this.frame.appendChild(this.frame.canvas);
  11625. if (!this.frame.canvas.getContext) {
  11626. var noCanvas = document.createElement( 'DIV' );
  11627. noCanvas.style.color = 'red';
  11628. noCanvas.style.fontWeight = 'bold' ;
  11629. noCanvas.style.padding = '10px';
  11630. noCanvas.innerHTML = 'Error: your browser does not support HTML canvas';
  11631. this.frame.canvas.appendChild(noCanvas);
  11632. }
  11633. var me = this;
  11634. this.drag = {};
  11635. this.pinch = {};
  11636. this.hammer = Hammer(this.frame.canvas, {
  11637. prevent_default: true
  11638. });
  11639. this.hammer.on('tap', me._onTap.bind(me) );
  11640. this.hammer.on('hold', me._onHold.bind(me) );
  11641. this.hammer.on('pinch', me._onPinch.bind(me) );
  11642. this.hammer.on('touch', me._onTouch.bind(me) );
  11643. this.hammer.on('dragstart', me._onDragStart.bind(me) );
  11644. this.hammer.on('drag', me._onDrag.bind(me) );
  11645. this.hammer.on('dragend', me._onDragEnd.bind(me) );
  11646. this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
  11647. this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
  11648. // add the frame to the container element
  11649. this.containerElement.appendChild(this.frame);
  11650. };
  11651. /**
  11652. *
  11653. * @param {{x: Number, y: Number}} pointer
  11654. * @return {Number | null} node
  11655. * @private
  11656. */
  11657. Graph.prototype._getNodeAt = function (pointer) {
  11658. var x = this._canvasToX(pointer.x);
  11659. var y = this._canvasToY(pointer.y);
  11660. var obj = {
  11661. left: x,
  11662. top: y,
  11663. right: x,
  11664. bottom: y
  11665. };
  11666. // if there are overlapping nodes, select the last one, this is the
  11667. // one which is drawn on top of the others
  11668. var overlappingNodes = this._getNodesOverlappingWith(obj);
  11669. return (overlappingNodes.length > 0) ?
  11670. overlappingNodes[overlappingNodes.length - 1] : null;
  11671. };
  11672. /**
  11673. * Get the pointer location from a touch location
  11674. * @param {{pageX: Number, pageY: Number}} touch
  11675. * @return {{x: Number, y: Number}} pointer
  11676. * @private
  11677. */
  11678. Graph.prototype._getPointer = function (touch) {
  11679. return {
  11680. x: touch.pageX - vis.util.getAbsoluteLeft(this.frame.canvas),
  11681. y: touch.pageY - vis.util.getAbsoluteTop(this.frame.canvas)
  11682. };
  11683. };
  11684. /**
  11685. * On start of a touch gesture, store the pointer
  11686. * @param event
  11687. * @private
  11688. */
  11689. Graph.prototype._onTouch = function (event) {
  11690. this.drag.pointer = this._getPointer(event.gesture.touches[0]);
  11691. this.drag.pinched = false;
  11692. this.pinch.scale = this._getScale();
  11693. };
  11694. /**
  11695. * handle drag start event
  11696. * @private
  11697. */
  11698. Graph.prototype._onDragStart = function () {
  11699. var drag = this.drag;
  11700. drag.selection = [];
  11701. drag.translation = this._getTranslation();
  11702. drag.nodeId = this._getNodeAt(drag.pointer);
  11703. // note: drag.pointer is set in _onTouch to get the initial touch location
  11704. var node = this.nodes[drag.nodeId];
  11705. if (node) {
  11706. // select the clicked node if not yet selected
  11707. if (!node.isSelected()) {
  11708. this._selectNodes([drag.nodeId]);
  11709. }
  11710. // create an array with the selected nodes and their original location and status
  11711. var me = this;
  11712. this.selection.forEach(function (id) {
  11713. var node = me.nodes[id];
  11714. if (node) {
  11715. var s = {
  11716. id: id,
  11717. node: node,
  11718. // store original x, y, xFixed and yFixed, make the node temporarily Fixed
  11719. x: node.x,
  11720. y: node.y,
  11721. xFixed: node.xFixed,
  11722. yFixed: node.yFixed
  11723. };
  11724. node.xFixed = true;
  11725. node.yFixed = true;
  11726. drag.selection.push(s);
  11727. }
  11728. });
  11729. }
  11730. };
  11731. /**
  11732. * handle drag event
  11733. * @private
  11734. */
  11735. Graph.prototype._onDrag = function (event) {
  11736. if (this.drag.pinched) {
  11737. return;
  11738. }
  11739. var pointer = this._getPointer(event.gesture.touches[0]);
  11740. var me = this,
  11741. drag = this.drag,
  11742. selection = drag.selection;
  11743. if (selection && selection.length) {
  11744. // calculate delta's and new location
  11745. var deltaX = pointer.x - drag.pointer.x,
  11746. deltaY = pointer.y - drag.pointer.y;
  11747. // update position of all selected nodes
  11748. selection.forEach(function (s) {
  11749. var node = s.node;
  11750. if (!s.xFixed) {
  11751. node.x = me._canvasToX(me._xToCanvas(s.x) + deltaX);
  11752. }
  11753. if (!s.yFixed) {
  11754. node.y = me._canvasToY(me._yToCanvas(s.y) + deltaY);
  11755. }
  11756. });
  11757. // start animation if not yet running
  11758. if (!this.moving) {
  11759. this.moving = true;
  11760. this.start();
  11761. }
  11762. }
  11763. else {
  11764. // move the graph
  11765. var diffX = pointer.x - this.drag.pointer.x;
  11766. var diffY = pointer.y - this.drag.pointer.y;
  11767. this._setTranslation(
  11768. this.drag.translation.x + diffX,
  11769. this.drag.translation.y + diffY);
  11770. this._redraw();
  11771. this.moved = true;
  11772. }
  11773. };
  11774. /**
  11775. * handle drag start event
  11776. * @private
  11777. */
  11778. Graph.prototype._onDragEnd = function () {
  11779. var selection = this.drag.selection;
  11780. if (selection) {
  11781. selection.forEach(function (s) {
  11782. // restore original xFixed and yFixed
  11783. s.node.xFixed = s.xFixed;
  11784. s.node.yFixed = s.yFixed;
  11785. });
  11786. }
  11787. };
  11788. /**
  11789. * handle tap/click event: select/unselect a node
  11790. * @private
  11791. */
  11792. Graph.prototype._onTap = function (event) {
  11793. var pointer = this._getPointer(event.gesture.touches[0]);
  11794. var nodeId = this._getNodeAt(pointer);
  11795. var node = this.nodes[nodeId];
  11796. if (node) {
  11797. // select this node
  11798. this._selectNodes([nodeId]);
  11799. if (!this.moving) {
  11800. this._redraw();
  11801. }
  11802. }
  11803. else {
  11804. // remove selection
  11805. this._unselectNodes();
  11806. this._redraw();
  11807. }
  11808. };
  11809. /**
  11810. * handle long tap event: multi select nodes
  11811. * @private
  11812. */
  11813. Graph.prototype._onHold = function (event) {
  11814. var pointer = this._getPointer(event.gesture.touches[0]);
  11815. var nodeId = this._getNodeAt(pointer);
  11816. var node = this.nodes[nodeId];
  11817. if (node) {
  11818. if (!node.isSelected()) {
  11819. // select this node, keep previous selection
  11820. var append = true;
  11821. this._selectNodes([nodeId], append);
  11822. }
  11823. else {
  11824. this._unselectNodes([nodeId]);
  11825. }
  11826. if (!this.moving) {
  11827. this._redraw();
  11828. }
  11829. }
  11830. else {
  11831. // Do nothing
  11832. }
  11833. };
  11834. /**
  11835. * Handle pinch event
  11836. * @param event
  11837. * @private
  11838. */
  11839. Graph.prototype._onPinch = function (event) {
  11840. var pointer = this._getPointer(event.gesture.center);
  11841. this.drag.pinched = true;
  11842. if (!('scale' in this.pinch)) {
  11843. this.pinch.scale = 1;
  11844. }
  11845. // TODO: enable moving while pinching?
  11846. var scale = this.pinch.scale * event.gesture.scale;
  11847. this._zoom(scale, pointer)
  11848. };
  11849. /**
  11850. * Zoom the graph in or out
  11851. * @param {Number} scale a number around 1, and between 0.01 and 10
  11852. * @param {{x: Number, y: Number}} pointer
  11853. * @return {Number} appliedScale scale is limited within the boundaries
  11854. * @private
  11855. */
  11856. Graph.prototype._zoom = function(scale, pointer) {
  11857. var scaleOld = this._getScale();
  11858. if (scale < 0.01) {
  11859. scale = 0.01;
  11860. }
  11861. if (scale > 10) {
  11862. scale = 10;
  11863. }
  11864. var translation = this._getTranslation();
  11865. var scaleFrac = scale / scaleOld;
  11866. var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
  11867. var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
  11868. this._setScale(scale);
  11869. this._setTranslation(tx, ty);
  11870. this._redraw();
  11871. return scale;
  11872. };
  11873. /**
  11874. * Event handler for mouse wheel event, used to zoom the timeline
  11875. * See http://adomas.org/javascript-mouse-wheel/
  11876. * https://github.com/EightMedia/hammer.js/issues/256
  11877. * @param {MouseEvent} event
  11878. * @private
  11879. */
  11880. Graph.prototype._onMouseWheel = function(event) {
  11881. // retrieve delta
  11882. var delta = 0;
  11883. if (event.wheelDelta) { /* IE/Opera. */
  11884. delta = event.wheelDelta/120;
  11885. } else if (event.detail) { /* Mozilla case. */
  11886. // In Mozilla, sign of delta is different than in IE.
  11887. // Also, delta is multiple of 3.
  11888. delta = -event.detail/3;
  11889. }
  11890. // If delta is nonzero, handle it.
  11891. // Basically, delta is now positive if wheel was scrolled up,
  11892. // and negative, if wheel was scrolled down.
  11893. if (delta) {
  11894. if (!('mouswheelScale' in this.pinch)) {
  11895. this.pinch.mouswheelScale = 1;
  11896. }
  11897. // calculate the new scale
  11898. var scale = this.pinch.mouswheelScale;
  11899. var zoom = delta / 10;
  11900. if (delta < 0) {
  11901. zoom = zoom / (1 - zoom);
  11902. }
  11903. scale *= (1 + zoom);
  11904. // calculate the pointer location
  11905. var gesture = Hammer.event.collectEventData(this, 'scroll', event);
  11906. var pointer = this._getPointer(gesture.center);
  11907. // apply the new scale
  11908. scale = this._zoom(scale, pointer);
  11909. // store the new, applied scale
  11910. this.pinch.mouswheelScale = scale;
  11911. }
  11912. // Prevent default actions caused by mouse wheel.
  11913. event.preventDefault();
  11914. };
  11915. /**
  11916. * Mouse move handler for checking whether the title moves over a node with a title.
  11917. * @param {Event} event
  11918. * @private
  11919. */
  11920. Graph.prototype._onMouseMoveTitle = function (event) {
  11921. var gesture = Hammer.event.collectEventData(this, 'mousemove', event);
  11922. var pointer = this._getPointer(gesture.center);
  11923. // check if the previously selected node is still selected
  11924. if (this.popupNode) {
  11925. this._checkHidePopup(pointer);
  11926. }
  11927. // start a timeout that will check if the mouse is positioned above
  11928. // an element
  11929. var me = this;
  11930. var checkShow = function() {
  11931. me._checkShowPopup(pointer);
  11932. };
  11933. if (this.popupTimer) {
  11934. clearInterval(this.popupTimer); // stop any running timer
  11935. }
  11936. if (!this.leftButtonDown) {
  11937. this.popupTimer = setTimeout(checkShow, 300);
  11938. }
  11939. };
  11940. /**
  11941. * Check if there is an element on the given position in the graph
  11942. * (a node or edge). If so, and if this element has a title,
  11943. * show a popup window with its title.
  11944. *
  11945. * @param {{x:Number, y:Number}} pointer
  11946. * @private
  11947. */
  11948. Graph.prototype._checkShowPopup = function (pointer) {
  11949. var obj = {
  11950. left: this._canvasToX(pointer.x),
  11951. top: this._canvasToY(pointer.y),
  11952. right: this._canvasToX(pointer.x),
  11953. bottom: this._canvasToY(pointer.y)
  11954. };
  11955. var id;
  11956. var lastPopupNode = this.popupNode;
  11957. if (this.popupNode == undefined) {
  11958. // search the nodes for overlap, select the top one in case of multiple nodes
  11959. var nodes = this.nodes;
  11960. for (id in nodes) {
  11961. if (nodes.hasOwnProperty(id)) {
  11962. var node = nodes[id];
  11963. if (node.getTitle() != undefined && node.isOverlappingWith(obj)) {
  11964. this.popupNode = node;
  11965. break;
  11966. }
  11967. }
  11968. }
  11969. }
  11970. if (this.popupNode == undefined) {
  11971. // search the edges for overlap
  11972. var edges = this.edges;
  11973. for (id in edges) {
  11974. if (edges.hasOwnProperty(id)) {
  11975. var edge = edges[id];
  11976. if (edge.connected && (edge.getTitle() != undefined) &&
  11977. edge.isOverlappingWith(obj)) {
  11978. this.popupNode = edge;
  11979. break;
  11980. }
  11981. }
  11982. }
  11983. }
  11984. if (this.popupNode) {
  11985. // show popup message window
  11986. if (this.popupNode != lastPopupNode) {
  11987. var me = this;
  11988. if (!me.popup) {
  11989. me.popup = new Popup(me.frame);
  11990. }
  11991. // adjust a small offset such that the mouse cursor is located in the
  11992. // bottom left location of the popup, and you can easily move over the
  11993. // popup area
  11994. me.popup.setPosition(pointer.x - 3, pointer.y - 3);
  11995. me.popup.setText(me.popupNode.getTitle());
  11996. me.popup.show();
  11997. }
  11998. }
  11999. else {
  12000. if (this.popup) {
  12001. this.popup.hide();
  12002. }
  12003. }
  12004. };
  12005. /**
  12006. * Check if the popup must be hided, which is the case when the mouse is no
  12007. * longer hovering on the object
  12008. * @param {{x:Number, y:Number}} pointer
  12009. * @private
  12010. */
  12011. Graph.prototype._checkHidePopup = function (pointer) {
  12012. if (!this.popupNode || !this._getNodeAt(pointer) ) {
  12013. this.popupNode = undefined;
  12014. if (this.popup) {
  12015. this.popup.hide();
  12016. }
  12017. }
  12018. };
  12019. /**
  12020. * Unselect selected nodes. If no selection array is provided, all nodes
  12021. * are unselected
  12022. * @param {Object[]} selection Array with selection objects, each selection
  12023. * object has a parameter row. Optional
  12024. * @param {Boolean} triggerSelect If true (default), the select event
  12025. * is triggered when nodes are unselected
  12026. * @return {Boolean} changed True if the selection is changed
  12027. * @private
  12028. */
  12029. Graph.prototype._unselectNodes = function(selection, triggerSelect) {
  12030. var changed = false;
  12031. var i, iMax, id;
  12032. if (selection) {
  12033. // remove provided selections
  12034. for (i = 0, iMax = selection.length; i < iMax; i++) {
  12035. id = selection[i];
  12036. this.nodes[id].unselect();
  12037. var j = 0;
  12038. while (j < this.selection.length) {
  12039. if (this.selection[j] == id) {
  12040. this.selection.splice(j, 1);
  12041. changed = true;
  12042. }
  12043. else {
  12044. j++;
  12045. }
  12046. }
  12047. }
  12048. }
  12049. else if (this.selection && this.selection.length) {
  12050. // remove all selections
  12051. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  12052. id = this.selection[i];
  12053. this.nodes[id].unselect();
  12054. changed = true;
  12055. }
  12056. this.selection = [];
  12057. }
  12058. if (changed && (triggerSelect == true || triggerSelect == undefined)) {
  12059. // fire the select event
  12060. this._trigger('select');
  12061. }
  12062. return changed;
  12063. };
  12064. /**
  12065. * select all nodes on given location x, y
  12066. * @param {Array} selection an array with node ids
  12067. * @param {boolean} append If true, the new selection will be appended to the
  12068. * current selection (except for duplicate entries)
  12069. * @return {Boolean} changed True if the selection is changed
  12070. * @private
  12071. */
  12072. Graph.prototype._selectNodes = function(selection, append) {
  12073. var changed = false;
  12074. var i, iMax;
  12075. // TODO: the selectNodes method is a little messy, rework this
  12076. // check if the current selection equals the desired selection
  12077. var selectionAlreadyThere = true;
  12078. if (selection.length != this.selection.length) {
  12079. selectionAlreadyThere = false;
  12080. }
  12081. else {
  12082. for (i = 0, iMax = Math.min(selection.length, this.selection.length); i < iMax; i++) {
  12083. if (selection[i] != this.selection[i]) {
  12084. selectionAlreadyThere = false;
  12085. break;
  12086. }
  12087. }
  12088. }
  12089. if (selectionAlreadyThere) {
  12090. return changed;
  12091. }
  12092. if (append == undefined || append == false) {
  12093. // first deselect any selected node
  12094. var triggerSelect = false;
  12095. changed = this._unselectNodes(undefined, triggerSelect);
  12096. }
  12097. for (i = 0, iMax = selection.length; i < iMax; i++) {
  12098. // add each of the new selections, but only when they are not duplicate
  12099. var id = selection[i];
  12100. var isDuplicate = (this.selection.indexOf(id) != -1);
  12101. if (!isDuplicate) {
  12102. this.nodes[id].select();
  12103. this.selection.push(id);
  12104. changed = true;
  12105. }
  12106. }
  12107. if (changed) {
  12108. // fire the select event
  12109. this._trigger('select');
  12110. }
  12111. return changed;
  12112. };
  12113. /**
  12114. * retrieve all nodes overlapping with given object
  12115. * @param {Object} obj An object with parameters left, top, right, bottom
  12116. * @return {Number[]} An array with id's of the overlapping nodes
  12117. * @private
  12118. */
  12119. Graph.prototype._getNodesOverlappingWith = function (obj) {
  12120. var nodes = this.nodes,
  12121. overlappingNodes = [];
  12122. for (var id in nodes) {
  12123. if (nodes.hasOwnProperty(id)) {
  12124. if (nodes[id].isOverlappingWith(obj)) {
  12125. overlappingNodes.push(id);
  12126. }
  12127. }
  12128. }
  12129. return overlappingNodes;
  12130. };
  12131. /**
  12132. * retrieve the currently selected nodes
  12133. * @return {Number[] | String[]} selection An array with the ids of the
  12134. * selected nodes.
  12135. */
  12136. Graph.prototype.getSelection = function() {
  12137. return this.selection.concat([]);
  12138. };
  12139. /**
  12140. * select zero or more nodes
  12141. * @param {Number[] | String[]} selection An array with the ids of the
  12142. * selected nodes.
  12143. */
  12144. Graph.prototype.setSelection = function(selection) {
  12145. var i, iMax, id;
  12146. if (!selection || (selection.length == undefined))
  12147. throw 'Selection must be an array with ids';
  12148. // first unselect any selected node
  12149. for (i = 0, iMax = this.selection.length; i < iMax; i++) {
  12150. id = this.selection[i];
  12151. this.nodes[id].unselect();
  12152. }
  12153. this.selection = [];
  12154. for (i = 0, iMax = selection.length; i < iMax; i++) {
  12155. id = selection[i];
  12156. var node = this.nodes[id];
  12157. if (!node) {
  12158. throw new RangeError('Node with id "' + id + '" not found');
  12159. }
  12160. node.select();
  12161. this.selection.push(id);
  12162. }
  12163. this.redraw();
  12164. };
  12165. /**
  12166. * Validate the selection: remove ids of nodes which no longer exist
  12167. * @private
  12168. */
  12169. Graph.prototype._updateSelection = function () {
  12170. var i = 0;
  12171. while (i < this.selection.length) {
  12172. var id = this.selection[i];
  12173. if (!this.nodes[id]) {
  12174. this.selection.splice(i, 1);
  12175. }
  12176. else {
  12177. i++;
  12178. }
  12179. }
  12180. };
  12181. /**
  12182. * Temporary method to test calculating a hub value for the nodes
  12183. * @param {number} level Maximum number edges between two nodes in order
  12184. * to call them connected. Optional, 1 by default
  12185. * @return {Number[]} connectioncount array with the connection count
  12186. * for each node
  12187. * @private
  12188. */
  12189. Graph.prototype._getConnectionCount = function(level) {
  12190. if (level == undefined) {
  12191. level = 1;
  12192. }
  12193. // get the nodes connected to given nodes
  12194. function getConnectedNodes(nodes) {
  12195. var connectedNodes = [];
  12196. for (var j = 0, jMax = nodes.length; j < jMax; j++) {
  12197. var node = nodes[j];
  12198. // find all nodes connected to this node
  12199. var edges = node.edges;
  12200. for (var i = 0, iMax = edges.length; i < iMax; i++) {
  12201. var edge = edges[i];
  12202. var other = null;
  12203. // check if connected
  12204. if (edge.from == node)
  12205. other = edge.to;
  12206. else if (edge.to == node)
  12207. other = edge.from;
  12208. // check if the other node is not already in the list with nodes
  12209. var k, kMax;
  12210. if (other) {
  12211. for (k = 0, kMax = nodes.length; k < kMax; k++) {
  12212. if (nodes[k] == other) {
  12213. other = null;
  12214. break;
  12215. }
  12216. }
  12217. }
  12218. if (other) {
  12219. for (k = 0, kMax = connectedNodes.length; k < kMax; k++) {
  12220. if (connectedNodes[k] == other) {
  12221. other = null;
  12222. break;
  12223. }
  12224. }
  12225. }
  12226. if (other)
  12227. connectedNodes.push(other);
  12228. }
  12229. }
  12230. return connectedNodes;
  12231. }
  12232. var connections = [];
  12233. var nodes = this.nodes;
  12234. for (var id in nodes) {
  12235. if (nodes.hasOwnProperty(id)) {
  12236. var c = [nodes[id]];
  12237. for (var l = 0; l < level; l++) {
  12238. c = c.concat(getConnectedNodes(c));
  12239. }
  12240. connections.push(c);
  12241. }
  12242. }
  12243. var hubs = [];
  12244. for (var i = 0, len = connections.length; i < len; i++) {
  12245. hubs.push(connections[i].length);
  12246. }
  12247. return hubs;
  12248. };
  12249. /**
  12250. * Set a new size for the graph
  12251. * @param {string} width Width in pixels or percentage (for example '800px'
  12252. * or '50%')
  12253. * @param {string} height Height in pixels or percentage (for example '400px'
  12254. * or '30%')
  12255. */
  12256. Graph.prototype.setSize = function(width, height) {
  12257. this.frame.style.width = width;
  12258. this.frame.style.height = height;
  12259. this.frame.canvas.style.width = '100%';
  12260. this.frame.canvas.style.height = '100%';
  12261. this.frame.canvas.width = this.frame.canvas.clientWidth;
  12262. this.frame.canvas.height = this.frame.canvas.clientHeight;
  12263. };
  12264. /**
  12265. * Set a data set with nodes for the graph
  12266. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  12267. * @private
  12268. */
  12269. Graph.prototype._setNodes = function(nodes) {
  12270. var oldNodesData = this.nodesData;
  12271. if (nodes instanceof DataSet || nodes instanceof DataView) {
  12272. this.nodesData = nodes;
  12273. }
  12274. else if (nodes instanceof Array) {
  12275. this.nodesData = new DataSet();
  12276. this.nodesData.add(nodes);
  12277. }
  12278. else if (!nodes) {
  12279. this.nodesData = new DataSet();
  12280. }
  12281. else {
  12282. throw new TypeError('Array or DataSet expected');
  12283. }
  12284. if (oldNodesData) {
  12285. // unsubscribe from old dataset
  12286. util.forEach(this.nodesListeners, function (callback, event) {
  12287. oldNodesData.unsubscribe(event, callback);
  12288. });
  12289. }
  12290. // remove drawn nodes
  12291. this.nodes = {};
  12292. if (this.nodesData) {
  12293. // subscribe to new dataset
  12294. var me = this;
  12295. util.forEach(this.nodesListeners, function (callback, event) {
  12296. me.nodesData.subscribe(event, callback);
  12297. });
  12298. // draw all new nodes
  12299. var ids = this.nodesData.getIds();
  12300. this._addNodes(ids);
  12301. }
  12302. this._updateSelection();
  12303. };
  12304. /**
  12305. * Add nodes
  12306. * @param {Number[] | String[]} ids
  12307. * @private
  12308. */
  12309. Graph.prototype._addNodes = function(ids) {
  12310. var id;
  12311. for (var i = 0, len = ids.length; i < len; i++) {
  12312. id = ids[i];
  12313. var data = this.nodesData.get(id);
  12314. var node = new Node(data, this.images, this.groups, this.constants);
  12315. this.nodes[id] = node; // note: this may replace an existing node
  12316. if (!node.isFixed()) {
  12317. // TODO: position new nodes in a smarter way!
  12318. var radius = this.constants.edges.length * 2;
  12319. var count = ids.length;
  12320. var angle = 2 * Math.PI * (i / count);
  12321. node.x = radius * Math.cos(angle);
  12322. node.y = radius * Math.sin(angle);
  12323. // note: no not use node.isMoving() here, as that gives the current
  12324. // velocity of the node, which is zero after creation of the node.
  12325. this.moving = true;
  12326. }
  12327. }
  12328. this._reconnectEdges();
  12329. this._updateValueRange(this.nodes);
  12330. };
  12331. /**
  12332. * Update existing nodes, or create them when not yet existing
  12333. * @param {Number[] | String[]} ids
  12334. * @private
  12335. */
  12336. Graph.prototype._updateNodes = function(ids) {
  12337. var nodes = this.nodes,
  12338. nodesData = this.nodesData;
  12339. for (var i = 0, len = ids.length; i < len; i++) {
  12340. var id = ids[i];
  12341. var node = nodes[id];
  12342. var data = nodesData.get(id);
  12343. if (node) {
  12344. // update node
  12345. node.setProperties(data, this.constants);
  12346. }
  12347. else {
  12348. // create node
  12349. node = new Node(properties, this.images, this.groups, this.constants);
  12350. nodes[id] = node;
  12351. if (!node.isFixed()) {
  12352. this.moving = true;
  12353. }
  12354. }
  12355. }
  12356. this._reconnectEdges();
  12357. this._updateValueRange(nodes);
  12358. };
  12359. /**
  12360. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  12361. * @param {Number[] | String[]} ids
  12362. * @private
  12363. */
  12364. Graph.prototype._removeNodes = function(ids) {
  12365. var nodes = this.nodes;
  12366. for (var i = 0, len = ids.length; i < len; i++) {
  12367. var id = ids[i];
  12368. delete nodes[id];
  12369. }
  12370. this._reconnectEdges();
  12371. this._updateSelection();
  12372. this._updateValueRange(nodes);
  12373. };
  12374. /**
  12375. * Load edges by reading the data table
  12376. * @param {Array | DataSet | DataView} edges The data containing the edges.
  12377. * @private
  12378. * @private
  12379. */
  12380. Graph.prototype._setEdges = function(edges) {
  12381. var oldEdgesData = this.edgesData;
  12382. if (edges instanceof DataSet || edges instanceof DataView) {
  12383. this.edgesData = edges;
  12384. }
  12385. else if (edges instanceof Array) {
  12386. this.edgesData = new DataSet();
  12387. this.edgesData.add(edges);
  12388. }
  12389. else if (!edges) {
  12390. this.edgesData = new DataSet();
  12391. }
  12392. else {
  12393. throw new TypeError('Array or DataSet expected');
  12394. }
  12395. if (oldEdgesData) {
  12396. // unsubscribe from old dataset
  12397. util.forEach(this.edgesListeners, function (callback, event) {
  12398. oldEdgesData.unsubscribe(event, callback);
  12399. });
  12400. }
  12401. // remove drawn edges
  12402. this.edges = {};
  12403. if (this.edgesData) {
  12404. // subscribe to new dataset
  12405. var me = this;
  12406. util.forEach(this.edgesListeners, function (callback, event) {
  12407. me.edgesData.subscribe(event, callback);
  12408. });
  12409. // draw all new nodes
  12410. var ids = this.edgesData.getIds();
  12411. this._addEdges(ids);
  12412. }
  12413. this._reconnectEdges();
  12414. };
  12415. /**
  12416. * Add edges
  12417. * @param {Number[] | String[]} ids
  12418. * @private
  12419. */
  12420. Graph.prototype._addEdges = function (ids) {
  12421. var edges = this.edges,
  12422. edgesData = this.edgesData;
  12423. for (var i = 0, len = ids.length; i < len; i++) {
  12424. var id = ids[i];
  12425. var oldEdge = edges[id];
  12426. if (oldEdge) {
  12427. oldEdge.disconnect();
  12428. }
  12429. var data = edgesData.get(id);
  12430. edges[id] = new Edge(data, this, this.constants);
  12431. }
  12432. this.moving = true;
  12433. this._updateValueRange(edges);
  12434. };
  12435. /**
  12436. * Update existing edges, or create them when not yet existing
  12437. * @param {Number[] | String[]} ids
  12438. * @private
  12439. */
  12440. Graph.prototype._updateEdges = function (ids) {
  12441. var edges = this.edges,
  12442. edgesData = this.edgesData;
  12443. for (var i = 0, len = ids.length; i < len; i++) {
  12444. var id = ids[i];
  12445. var data = edgesData.get(id);
  12446. var edge = edges[id];
  12447. if (edge) {
  12448. // update edge
  12449. edge.disconnect();
  12450. edge.setProperties(data, this.constants);
  12451. edge.connect();
  12452. }
  12453. else {
  12454. // create edge
  12455. edge = new Edge(data, this, this.constants);
  12456. this.edges[id] = edge;
  12457. }
  12458. }
  12459. this.moving = true;
  12460. this._updateValueRange(edges);
  12461. };
  12462. /**
  12463. * Remove existing edges. Non existing ids will be ignored
  12464. * @param {Number[] | String[]} ids
  12465. * @private
  12466. */
  12467. Graph.prototype._removeEdges = function (ids) {
  12468. var edges = this.edges;
  12469. for (var i = 0, len = ids.length; i < len; i++) {
  12470. var id = ids[i];
  12471. var edge = edges[id];
  12472. if (edge) {
  12473. edge.disconnect();
  12474. delete edges[id];
  12475. }
  12476. }
  12477. this.moving = true;
  12478. this._updateValueRange(edges);
  12479. };
  12480. /**
  12481. * Reconnect all edges
  12482. * @private
  12483. */
  12484. Graph.prototype._reconnectEdges = function() {
  12485. var id,
  12486. nodes = this.nodes,
  12487. edges = this.edges;
  12488. for (id in nodes) {
  12489. if (nodes.hasOwnProperty(id)) {
  12490. nodes[id].edges = [];
  12491. }
  12492. }
  12493. for (id in edges) {
  12494. if (edges.hasOwnProperty(id)) {
  12495. var edge = edges[id];
  12496. edge.from = null;
  12497. edge.to = null;
  12498. edge.connect();
  12499. }
  12500. }
  12501. };
  12502. /**
  12503. * Update the values of all object in the given array according to the current
  12504. * value range of the objects in the array.
  12505. * @param {Object} obj An object containing a set of Edges or Nodes
  12506. * The objects must have a method getValue() and
  12507. * setValueRange(min, max).
  12508. * @private
  12509. */
  12510. Graph.prototype._updateValueRange = function(obj) {
  12511. var id;
  12512. // determine the range of the objects
  12513. var valueMin = undefined;
  12514. var valueMax = undefined;
  12515. for (id in obj) {
  12516. if (obj.hasOwnProperty(id)) {
  12517. var value = obj[id].getValue();
  12518. if (value !== undefined) {
  12519. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  12520. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  12521. }
  12522. }
  12523. }
  12524. // adjust the range of all objects
  12525. if (valueMin !== undefined && valueMax !== undefined) {
  12526. for (id in obj) {
  12527. if (obj.hasOwnProperty(id)) {
  12528. obj[id].setValueRange(valueMin, valueMax);
  12529. }
  12530. }
  12531. }
  12532. };
  12533. /**
  12534. * Redraw the graph with the current data
  12535. * chart will be resized too.
  12536. */
  12537. Graph.prototype.redraw = function() {
  12538. this.setSize(this.width, this.height);
  12539. this._redraw();
  12540. };
  12541. /**
  12542. * Redraw the graph with the current data
  12543. * @private
  12544. */
  12545. Graph.prototype._redraw = function() {
  12546. var ctx = this.frame.canvas.getContext('2d');
  12547. // clear the canvas
  12548. var w = this.frame.canvas.width;
  12549. var h = this.frame.canvas.height;
  12550. ctx.clearRect(0, 0, w, h);
  12551. // set scaling and translation
  12552. ctx.save();
  12553. ctx.translate(this.translation.x, this.translation.y);
  12554. ctx.scale(this.scale, this.scale);
  12555. this._drawEdges(ctx);
  12556. this._drawNodes(ctx);
  12557. // restore original scaling and translation
  12558. ctx.restore();
  12559. };
  12560. /**
  12561. * Set the translation of the graph
  12562. * @param {Number} offsetX Horizontal offset
  12563. * @param {Number} offsetY Vertical offset
  12564. * @private
  12565. */
  12566. Graph.prototype._setTranslation = function(offsetX, offsetY) {
  12567. if (this.translation === undefined) {
  12568. this.translation = {
  12569. x: 0,
  12570. y: 0
  12571. };
  12572. }
  12573. if (offsetX !== undefined) {
  12574. this.translation.x = offsetX;
  12575. }
  12576. if (offsetY !== undefined) {
  12577. this.translation.y = offsetY;
  12578. }
  12579. };
  12580. /**
  12581. * Get the translation of the graph
  12582. * @return {Object} translation An object with parameters x and y, both a number
  12583. * @private
  12584. */
  12585. Graph.prototype._getTranslation = function() {
  12586. return {
  12587. x: this.translation.x,
  12588. y: this.translation.y
  12589. };
  12590. };
  12591. /**
  12592. * Scale the graph
  12593. * @param {Number} scale Scaling factor 1.0 is unscaled
  12594. * @private
  12595. */
  12596. Graph.prototype._setScale = function(scale) {
  12597. this.scale = scale;
  12598. };
  12599. /**
  12600. * Get the current scale of the graph
  12601. * @return {Number} scale Scaling factor 1.0 is unscaled
  12602. * @private
  12603. */
  12604. Graph.prototype._getScale = function() {
  12605. return this.scale;
  12606. };
  12607. /**
  12608. * Convert a horizontal point on the HTML canvas to the x-value of the model
  12609. * @param {number} x
  12610. * @returns {number}
  12611. * @private
  12612. */
  12613. Graph.prototype._canvasToX = function(x) {
  12614. return (x - this.translation.x) / this.scale;
  12615. };
  12616. /**
  12617. * Convert an x-value in the model to a horizontal point on the HTML canvas
  12618. * @param {number} x
  12619. * @returns {number}
  12620. * @private
  12621. */
  12622. Graph.prototype._xToCanvas = function(x) {
  12623. return x * this.scale + this.translation.x;
  12624. };
  12625. /**
  12626. * Convert a vertical point on the HTML canvas to the y-value of the model
  12627. * @param {number} y
  12628. * @returns {number}
  12629. * @private
  12630. */
  12631. Graph.prototype._canvasToY = function(y) {
  12632. return (y - this.translation.y) / this.scale;
  12633. };
  12634. /**
  12635. * Convert an y-value in the model to a vertical point on the HTML canvas
  12636. * @param {number} y
  12637. * @returns {number}
  12638. * @private
  12639. */
  12640. Graph.prototype._yToCanvas = function(y) {
  12641. return y * this.scale + this.translation.y ;
  12642. };
  12643. /**
  12644. * Redraw all nodes
  12645. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  12646. * @param {CanvasRenderingContext2D} ctx
  12647. * @private
  12648. */
  12649. Graph.prototype._drawNodes = function(ctx) {
  12650. // first draw the unselected nodes
  12651. var nodes = this.nodes;
  12652. var selected = [];
  12653. for (var id in nodes) {
  12654. if (nodes.hasOwnProperty(id)) {
  12655. if (nodes[id].isSelected()) {
  12656. selected.push(id);
  12657. }
  12658. else {
  12659. nodes[id].draw(ctx);
  12660. }
  12661. }
  12662. }
  12663. // draw the selected nodes on top
  12664. for (var s = 0, sMax = selected.length; s < sMax; s++) {
  12665. nodes[selected[s]].draw(ctx);
  12666. }
  12667. };
  12668. /**
  12669. * Redraw all edges
  12670. * The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d');
  12671. * @param {CanvasRenderingContext2D} ctx
  12672. * @private
  12673. */
  12674. Graph.prototype._drawEdges = function(ctx) {
  12675. var edges = this.edges;
  12676. for (var id in edges) {
  12677. if (edges.hasOwnProperty(id)) {
  12678. var edge = edges[id];
  12679. if (edge.connected) {
  12680. edges[id].draw(ctx);
  12681. }
  12682. }
  12683. }
  12684. };
  12685. /**
  12686. * Find a stable position for all nodes
  12687. * @private
  12688. */
  12689. Graph.prototype._doStabilize = function() {
  12690. var start = new Date();
  12691. // find stable position
  12692. var count = 0;
  12693. var vmin = this.constants.minVelocity;
  12694. var stable = false;
  12695. while (!stable && count < this.constants.maxIterations) {
  12696. this._calculateForces();
  12697. this._discreteStepNodes();
  12698. stable = !this._isMoving(vmin);
  12699. count++;
  12700. }
  12701. var end = new Date();
  12702. // console.log('Stabilized in ' + (end-start) + ' ms, ' + count + ' iterations' ); // TODO: cleanup
  12703. };
  12704. /**
  12705. * Calculate the external forces acting on the nodes
  12706. * Forces are caused by: edges, repulsing forces between nodes, gravity
  12707. * @private
  12708. */
  12709. Graph.prototype._calculateForces = function() {
  12710. // create a local edge to the nodes and edges, that is faster
  12711. var id, dx, dy, angle, distance, fx, fy,
  12712. repulsingForce, springForce, length, edgeLength,
  12713. nodes = this.nodes,
  12714. edges = this.edges;
  12715. // gravity, add a small constant force to pull the nodes towards the center of
  12716. // the graph
  12717. // Also, the forces are reset to zero in this loop by using _setForce instead
  12718. // of _addForce
  12719. var gravity = 0.01,
  12720. gx = this.frame.canvas.clientWidth / 2,
  12721. gy = this.frame.canvas.clientHeight / 2;
  12722. for (id in nodes) {
  12723. if (nodes.hasOwnProperty(id)) {
  12724. var node = nodes[id];
  12725. dx = gx - node.x;
  12726. dy = gy - node.y;
  12727. angle = Math.atan2(dy, dx);
  12728. fx = Math.cos(angle) * gravity;
  12729. fy = Math.sin(angle) * gravity;
  12730. node._setForce(fx, fy);
  12731. }
  12732. }
  12733. // repulsing forces between nodes
  12734. var minimumDistance = this.constants.nodes.distance,
  12735. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  12736. for (var id1 in nodes) {
  12737. if (nodes.hasOwnProperty(id1)) {
  12738. var node1 = nodes[id1];
  12739. for (var id2 in nodes) {
  12740. if (nodes.hasOwnProperty(id2)) {
  12741. var node2 = nodes[id2];
  12742. // calculate normally distributed force
  12743. dx = node2.x - node1.x;
  12744. dy = node2.y - node1.y;
  12745. distance = Math.sqrt(dx * dx + dy * dy);
  12746. angle = Math.atan2(dy, dx);
  12747. // TODO: correct factor for repulsing force
  12748. //repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12749. //repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12750. repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
  12751. fx = Math.cos(angle) * repulsingForce;
  12752. fy = Math.sin(angle) * repulsingForce;
  12753. node1._addForce(-fx, -fy);
  12754. node2._addForce(fx, fy);
  12755. }
  12756. }
  12757. }
  12758. }
  12759. /* TODO: re-implement repulsion of edges
  12760. for (var n = 0; n < nodes.length; n++) {
  12761. for (var l = 0; l < edges.length; l++) {
  12762. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  12763. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  12764. // calculate normally distributed force
  12765. dx = nodes[n].x - lx,
  12766. dy = nodes[n].y - ly,
  12767. distance = Math.sqrt(dx * dx + dy * dy),
  12768. angle = Math.atan2(dy, dx),
  12769. // TODO: correct factor for repulsing force
  12770. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12771. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  12772. repulsingforce = 1 / (1 + Math.exp((distance / (minimumDistance / 2) - 1) * steepness)), // TODO: customize the repulsing force
  12773. fx = Math.cos(angle) * repulsingforce,
  12774. fy = Math.sin(angle) * repulsingforce;
  12775. nodes[n]._addForce(fx, fy);
  12776. edges[l].from._addForce(-fx/2,-fy/2);
  12777. edges[l].to._addForce(-fx/2,-fy/2);
  12778. }
  12779. }
  12780. */
  12781. // forces caused by the edges, modelled as springs
  12782. for (id in edges) {
  12783. if (edges.hasOwnProperty(id)) {
  12784. var edge = edges[id];
  12785. if (edge.connected) {
  12786. dx = (edge.to.x - edge.from.x);
  12787. dy = (edge.to.y - edge.from.y);
  12788. //edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
  12789. //edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
  12790. //edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
  12791. edgeLength = edge.length;
  12792. length = Math.sqrt(dx * dx + dy * dy);
  12793. angle = Math.atan2(dy, dx);
  12794. springForce = edge.stiffness * (edgeLength - length);
  12795. fx = Math.cos(angle) * springForce;
  12796. fy = Math.sin(angle) * springForce;
  12797. edge.from._addForce(-fx, -fy);
  12798. edge.to._addForce(fx, fy);
  12799. }
  12800. }
  12801. }
  12802. /* TODO: re-implement repulsion of edges
  12803. // repulsing forces between edges
  12804. var minimumDistance = this.constants.edges.distance,
  12805. steepness = 10; // higher value gives steeper slope of the force around the given minimumDistance
  12806. for (var l = 0; l < edges.length; l++) {
  12807. //Keep distance from other edge centers
  12808. for (var l2 = l + 1; l2 < this.edges.length; l2++) {
  12809. //var dmin = (nodes[n].width + nodes[n].height + nodes[n2].width + nodes[n2].height) / 1 || minimumDistance, // TODO: dmin
  12810. //var dmin = (nodes[n].width + nodes[n2].width)/2 || minimumDistance, // TODO: dmin
  12811. //dmin = 40 + ((nodes[n].width/2 + nodes[n2].width/2) || 0),
  12812. var lx = edges[l].from.x+(edges[l].to.x - edges[l].from.x)/2,
  12813. ly = edges[l].from.y+(edges[l].to.y - edges[l].from.y)/2,
  12814. l2x = edges[l2].from.x+(edges[l2].to.x - edges[l2].from.x)/2,
  12815. l2y = edges[l2].from.y+(edges[l2].to.y - edges[l2].from.y)/2,
  12816. // calculate normally distributed force
  12817. dx = l2x - lx,
  12818. dy = l2y - ly,
  12819. distance = Math.sqrt(dx * dx + dy * dy),
  12820. angle = Math.atan2(dy, dx),
  12821. // TODO: correct factor for repulsing force
  12822. //var repulsingforce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
  12823. //repulsingforce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ), // TODO: customize the repulsing force
  12824. repulsingforce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)), // TODO: customize the repulsing force
  12825. fx = Math.cos(angle) * repulsingforce,
  12826. fy = Math.sin(angle) * repulsingforce;
  12827. edges[l].from._addForce(-fx, -fy);
  12828. edges[l].to._addForce(-fx, -fy);
  12829. edges[l2].from._addForce(fx, fy);
  12830. edges[l2].to._addForce(fx, fy);
  12831. }
  12832. }
  12833. */
  12834. };
  12835. /**
  12836. * Check if any of the nodes is still moving
  12837. * @param {number} vmin the minimum velocity considered as 'moving'
  12838. * @return {boolean} true if moving, false if non of the nodes is moving
  12839. * @private
  12840. */
  12841. Graph.prototype._isMoving = function(vmin) {
  12842. // TODO: ismoving does not work well: should check the kinetic energy, not its velocity
  12843. var nodes = this.nodes;
  12844. for (var id in nodes) {
  12845. if (nodes.hasOwnProperty(id) && nodes[id].isMoving(vmin)) {
  12846. return true;
  12847. }
  12848. }
  12849. return false;
  12850. };
  12851. /**
  12852. * Perform one discrete step for all nodes
  12853. * @private
  12854. */
  12855. Graph.prototype._discreteStepNodes = function() {
  12856. var interval = this.refreshRate / 1000.0; // in seconds
  12857. var nodes = this.nodes;
  12858. for (var id in nodes) {
  12859. if (nodes.hasOwnProperty(id)) {
  12860. nodes[id].discreteStep(interval);
  12861. }
  12862. }
  12863. };
  12864. /**
  12865. * Start animating nodes and edges
  12866. */
  12867. Graph.prototype.start = function() {
  12868. if (this.moving) {
  12869. this._calculateForces();
  12870. this._discreteStepNodes();
  12871. var vmin = this.constants.minVelocity;
  12872. this.moving = this._isMoving(vmin);
  12873. }
  12874. if (this.moving) {
  12875. // start animation. only start timer if it is not already running
  12876. if (!this.timer) {
  12877. var graph = this;
  12878. this.timer = window.setTimeout(function () {
  12879. graph.timer = undefined;
  12880. graph.start();
  12881. graph._redraw();
  12882. }, this.refreshRate);
  12883. }
  12884. }
  12885. else {
  12886. this._redraw();
  12887. }
  12888. };
  12889. /**
  12890. * Stop animating nodes and edges.
  12891. */
  12892. Graph.prototype.stop = function () {
  12893. if (this.timer) {
  12894. window.clearInterval(this.timer);
  12895. this.timer = undefined;
  12896. }
  12897. };
  12898. /**
  12899. * vis.js module exports
  12900. */
  12901. var vis = {
  12902. util: util,
  12903. events: events,
  12904. Controller: Controller,
  12905. DataSet: DataSet,
  12906. DataView: DataView,
  12907. Range: Range,
  12908. Stack: Stack,
  12909. TimeStep: TimeStep,
  12910. EventBus: EventBus,
  12911. components: {
  12912. items: {
  12913. Item: Item,
  12914. ItemBox: ItemBox,
  12915. ItemPoint: ItemPoint,
  12916. ItemRange: ItemRange
  12917. },
  12918. Component: Component,
  12919. Panel: Panel,
  12920. RootPanel: RootPanel,
  12921. ItemSet: ItemSet,
  12922. TimeAxis: TimeAxis
  12923. },
  12924. graph: {
  12925. Node: Node,
  12926. Edge: Edge,
  12927. Popup: Popup,
  12928. Groups: Groups,
  12929. Images: Images
  12930. },
  12931. Timeline: Timeline,
  12932. Graph: Graph
  12933. };
  12934. /**
  12935. * CommonJS module exports
  12936. */
  12937. if (typeof exports !== 'undefined') {
  12938. exports = vis;
  12939. }
  12940. if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
  12941. module.exports = vis;
  12942. }
  12943. /**
  12944. * AMD module exports
  12945. */
  12946. if (typeof(define) === 'function') {
  12947. define(function () {
  12948. return vis;
  12949. });
  12950. }
  12951. /**
  12952. * Window exports
  12953. */
  12954. if (typeof window !== 'undefined') {
  12955. // attach the module to the window, load as a regular javascript file
  12956. window['vis'] = vis;
  12957. }
  12958. // inject css
  12959. util.loadCss("/* vis.js stylesheet */\n.vis.timeline {\n}\n\n\n.vis.timeline.rootpanel {\n position: relative;\n overflow: hidden;\n\n border: 1px solid #bfbfbf;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n\n.vis.timeline .panel {\n position: absolute;\n overflow: hidden;\n}\n\n\n.vis.timeline .groupset {\n position: absolute;\n padding: 0;\n margin: 0;\n}\n\n.vis.timeline .labels {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n\n padding: 0;\n margin: 0;\n\n border-right: 1px solid #bfbfbf;\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n}\n\n.vis.timeline .labels .label {\n position: absolute;\n left: 0;\n top: 0;\n width: 100%;\n border-bottom: 1px solid #bfbfbf;\n color: #4d4d4d;\n}\n\n.vis.timeline .labels .label .inner {\n display: inline-block;\n padding: 5px;\n}\n\n\n.vis.timeline .itemset {\n position: absolute;\n padding: 0;\n margin: 0;\n overflow: hidden;\n}\n\n.vis.timeline .background {\n}\n\n.vis.timeline .foreground {\n}\n\n.vis.timeline .itemset-axis {\n position: absolute;\n}\n\n.vis.timeline .groupset .itemset-axis {\n border-top: 1px solid #bfbfbf;\n}\n\n/* TODO: with orientation=='bottom', this will more or less overlap with timeline axis\n.vis.timeline .groupset .itemset-axis:last-child {\n border-top: none;\n}\n*/\n\n\n.vis.timeline .item {\n position: absolute;\n color: #1A1A1A;\n border-color: #97B0F8;\n background-color: #D5DDF6;\n display: inline-block;\n}\n\n.vis.timeline .item.selected {\n border-color: #FFC200;\n background-color: #FFF785;\n z-index: 999;\n}\n\n.vis.timeline .item.cluster {\n /* TODO: use another color or pattern? */\n background: #97B0F8 url('img/cluster_bg.png');\n color: white;\n}\n.vis.timeline .item.cluster.point {\n border-color: #D5DDF6;\n}\n\n.vis.timeline .item.box {\n text-align: center;\n border-style: solid;\n border-width: 1px;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.point {\n background: none;\n}\n\n.vis.timeline .dot {\n border: 5px solid #97B0F8;\n position: absolute;\n border-radius: 5px;\n -moz-border-radius: 5px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range {\n overflow: hidden;\n border-style: solid;\n border-width: 1px;\n border-radius: 2px;\n -moz-border-radius: 2px; /* For Firefox 3.6 and older */\n}\n\n.vis.timeline .item.range .drag-left {\n cursor: w-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .drag-right {\n cursor: e-resize;\n z-index: 1000;\n}\n\n.vis.timeline .item.range .content {\n position: relative;\n display: inline-block;\n}\n\n.vis.timeline .item.line {\n position: absolute;\n width: 0;\n border-left-width: 1px;\n border-left-style: solid;\n}\n\n.vis.timeline .item .content {\n margin: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.vis.timeline .axis {\n position: relative;\n}\n\n.vis.timeline .axis .text {\n position: absolute;\n color: #4d4d4d;\n padding: 3px;\n white-space: nowrap;\n}\n\n.vis.timeline .axis .text.measure {\n position: absolute;\n padding-left: 0;\n padding-right: 0;\n margin-left: 0;\n margin-right: 0;\n visibility: hidden;\n}\n\n.vis.timeline .axis .grid.vertical {\n position: absolute;\n width: 0;\n border-right: 1px solid;\n}\n\n.vis.timeline .axis .grid.horizontal {\n position: absolute;\n left: 0;\n width: 100%;\n height: 0;\n border-bottom: 1px solid;\n}\n\n.vis.timeline .axis .grid.minor {\n border-color: #e5e5e5;\n}\n\n.vis.timeline .axis .grid.major {\n border-color: #bfbfbf;\n}\n\n");
  12960. },{"hammerjs":1,"moment":2}]},{},[3])
  12961. (3)
  12962. });
  12963. ;